• WPF: Деревья и колонки

    Одним из часто используемых элементов управления является дерево, оснащенное дополнительными колонками для детального представления элементов.

    clip_image002

    Библиотека WPF, на первый взгляд, не содержит такого элемента. ;-)

    Но существует несколько сторонних разработок, позволяющих добиться такого эффекта.

    1. 1. Telerik RadTreeListView. Элемент управления. созданный компанией Telerik, не имеющий ничего общего с System.Windows.Controls.TreeView и напрямую наследующий System.Windows.Controls.ItemsControl. Прямо из коробки этот элемент не содержит колонок. Но после небольшой доработки, предложенной автором(http://www.telerik.com/community/forums/wpf/treeview/multicolumn-treeview.aspx) этот элемент превращается в довольно симпатичное дерево с колонками. Колонки задаются довольно просто.
    2. CodeProject: TreeListView (http://www.codeproject.com/KB/WPF/TreeListView.aspx) Это основанный на System.Windows.Controls.TreeView элемент управления который содержит несколько десятков строк кода на С#, вместе с замысловатым в котором вычисляют насколько необходимо сдвинуть строку от левого края в зависимости от уровня элемента дерева. Через этот конвертор затем выполняют привязку ширины фиктивного элемента осуществляющего сдвиг. Кроме того элемент переопределяет почти все шаблоны из TreeView. Неплохой вариант, поддерживает даже изменения порядка колонок, но слишком уж сложно все организовано.
    3. Mindscape MulticolumnTreeView (http://www.mindscape.co.nz/) Показывает колонки прямо из коробки, никаких доработок не требует. Но опять-таки стоит денег.

    Тут я предлагаю вспомнить о том, что элементы управления в WPF не имеют жестко заданного внешнего вид, а имеют только поведение и дерево это нечто, что содержит иерархию элементов, а как выглядеть этим элементам решать разработчику. Для того, чтобы добавить колонки в обычное дерево мы переопределим его ItemTemplate:

    <TreeView.ItemTemplate>

      <HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">

        <Grid>

          <Grid.Resources>

            <Style TargetType="TextBlock">

              <Setter Property="Margin" Value="1"/>

            </Style>

          </Grid.Resources>

          <Grid.ColumnDefinitions>

            <ColumnDefinition Width="*"/>

            <ColumnDefinition Width="Auto"/>

            <ColumnDefinition Width="Auto"/>

            <ColumnDefinition Width="Auto"/>

          </Grid.ColumnDefinitions>

            <TextBlock Text="{Binding Path=Title}" Grid.Column="0" />

            <TextBlock Text="{Binding Path=DateTime}" Grid.Column="1"

                  Width="{Binding ElementName=DateTimeHeader, Path=ActualWidth}"/>

            <TextBlock Text="{Binding Path=Raiting}" Grid.Column="2"

                  Width="{Binding ElementName=RaitingHeader, Path=ActualWidth}"/>

            <Rectangle Width="{Binding ElementName=EmptyHeader, Path=ActualWidth}" Grid.Column="3" />

        </Grid>

      </HierarchicalDataTemplate>

    </TreeView.ItemTemplate>

    Затем добавим заголовок, чтобы отображать названия колонок и изменять их размер.

    <Control.Template>

      <ControlTemplate>

        <StackPanel>

          <Grid x:Name="HeaderGrid">

            <Grid.Resources>

              <Style TargetType="GridViewColumnHeader">

                <Setter Property="Margin" Value="1"/>

              </Style>

            </Grid.Resources>

            <Grid.ColumnDefinitions>

              <ColumnDefinition Width="Auto"/>

              <ColumnDefinition Width="Auto"/>

              <ColumnDefinition Width="Auto"/>

              <ColumnDefinition Width="*" />

            </Grid.ColumnDefinitions>

            <GridViewColumnHeader Content="Title"   Grid.Column="0" x:Name="TitleHeader" Width="220" />

            <GridViewColumnHeader Content="Date/Time" Grid.Column="1" x:Name="DateTimeHeader" />

            <GridViewColumnHeader Content="Raiting" Grid.Column="2" x:Name="RaitingHeader" />

            <Rectangle Grid.Column="3" x:Name="EmptyHeader" />

          </Grid>

          <ItemsPresenter Width="{Binding ElementName=HeaderGrid, Path=ActualWidth}" />

        </StackPanel>

      </ControlTemplate>

    </Control.Template>

    В этом коде на XAML есть одна небольшая хитрость: в заголовке последняя колонка элемента Grid заполняет все оставшееся место, а в шаблоне элемента дерева первая (ведь у каждого элемента она разная, т.к. элементы разных уровней по-разному отодвинуты от левого края). Кроме того добавлен прямоугольник заполняющий свободную область справа и выполнены соответствующие привязки ширины элементов и данных в элементах TextBlock. На этом казалось бы можно остановится, но шаблон элемента TreeViewItem из которого строится дерево не растягивает свое содержимое на все доступное пространство. Поэтому его придется переопределить. Возьмем его стандартный шаблон и изменим его всего в двух местах.

    В единственном элементе Grid вместо

    <Grid.ColumnDefinitions>

      <ColumnDefinition Width="Auto" />

      <ColumnDefinition Width="Auto" />

      <ColumnDefinition Width="*" />

    </Grid.ColumnDefinitions>

    оставляем только две колонки

    <Grid.ColumnDefinitions>

      <ColumnDefinition Width="Auto" />

      <ColumnDefinition Width="*" />

    </Grid.ColumnDefinitions>

    И в элементе ContentPresenter устанавливаем HorizontalAlignment="Stretch".

    Добавим мелкие изменения, связанные с удалением одной колонки. Не забываем также вставить привязку

    IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource Mode=TemplatedParent, AncestorType={x:Null}}}"

    в элемент TogleButton. Она почему-то не видна в шаблоне, если его извлекать при помощи XamlWriter и свойства Template соответствующего элемента управления.

    Вот и все наш элемент готов и без единой строчки Code-Behind.

    clip_image004

    Полный код примера здесь: DetailedTree или здесь: http://cid-c22b7611bb9d0310.skydrive.live.com/browse.aspx/Samples

    Надіслане 24-11-2009 12:04 від Dmitry Peleshenko | 5 коментарів
    Зареєстрований як
  • Еще одно различие в WPF и WindowsForms

    Еще несколько слов о различиях в обработке фокуса в WPF и WindowsForms. Не могу удержаться, т.к. мои коллеги убеждают меня в том, что переходя на WPF, разработчик обязательно столкнется с неразрешимыми проблемами вроде той, что я описал в предыдущем посте.

    Итак, снова приложение с несколькими вкладками. Понятно, что для быстрой и комфортной работы пользователя необходимо устанавливать фокус ввода на какой-то элемент при выборе вкладки, да и при запуске приложения тоже.

    В старом добром WindowsForms как бы мы не расставляли значения своств TabIndex и TabStop, фокус все равно придется ставить вручную в каком-то обработчике событий (например, в SelectedIndexChanged элемента TabControl). Получается что вроде этого:

    private void tabControl_SelectedIndexChanged(object sender, EventArgs e)
    {
       if (tabControl.SelectedTab == myPage)
       {
          textBoxFromMyPage.Focus();
       }
    }

    В WPF все несколько иначе (и на мой взгляд логичнее), если у элемента расположенного на TabItem самый маленький TabIndex, то на него и будет установлен фокус ввода при выборе вкладки. Т.е. поведение фокуса ввода можно полностью задать в дизайнере, без единой строчки кода.

    Единственное в чем может возникнуть затруднение это в установке фокуса в момент запуска приложения, но эта проблема также решается без обработки событий. Просто для одного из контейнеров (обычного того, содержимое которого будет видеть пользователь при запуске) следует установить свойство FocusManager.FocusedElement. Получится следующий код на XAML:

    <TabControl>
       <TabItem Header="MyFirstTab">
          <WrapPanel FocusManager.FocusedElement="{Binding ElementName=myTextBox}">
             <Button TabIndex="1">The Button</Button>
             <TextBox x:Name="myTextBox" TabIndex="0">The Text Box</TextBox>
          </WrapPanel>
       </TabItem>
       <TabItem Header="MySeccondTab">
          <WrapPanel>
             <Button TabIndex="1">The Button</Button>
    ...

    И все без единой строчки на С# или VB.Net.

    Надіслане 02-11-2009 10:25 від Dmitry Peleshenko | 0 коментарів
    Зареєстрований як
  • Взаимодействие WPF и WindowsForms

    Если Вы пытаетесь применять WPF не в абсолютно новом, а в прожившем какое-то время проекте, то наверняка столкнетесь с необходимостью взаимодействия между кусочками старого интерфейса на WindowsForms и новым интерфейсом WPF. Так и я некоторое время назад столкнулся с этой проблемой.

    Задача заключалась в усовершенствовании части системы, которая включала в себя интерфейс на WindowsForms с несколькими вкладками, каждая из которых содержала UserControl. Переход на WPF следовало осуществить постепенно заменяя старые WindowsForms элементы новыми. Первым делом я создал макет бедующего приложения - окно с TabControl и две вкладки одна для WPF, другая для WindowsForms. На вкладке с WindowsForms размещен UserControl с несколькими элементами управления, занимающий всю вкладку.

    <TabControl SelectedIndex>
       <TabItem Header="WPF">
          <WrapPanel>
             <Button TabIndex="1">The Button</Button>
             <TextBox TabIndex="0">The Text Box</TextBox>
          </WrapPanel>
       </TabItem>
       <TabItem Header="Windows Form" x:Name="windowsFormsTab">
          <integration:WindowsFormsHost>
             <this:TestControl x:Name="windowsFormsControl" />
          </integration:WindowsFormsHost>
       </TabItem>
    </TabControl>

    В старом UserControl содержалось текстовое поле, на которое должен был быть установлен фокус ввода при выборе содержащей его вкладки. В WindowsForms эта проблема решалась обработкой события VisibleChanged класса UserControl.

    if (Visible)
    {
       // Необходимо было также выделить все содержимое
       textBox.SelectAll();
       textBox.Focus();
    }

    В новом приложении этот подход работать перестал. Более того, при переключении на окна других приложений элементы WindowsForms безвозвратно теряли фокус.

    Попытка обрабатывать события Selector.Selected или GotFocus элемента TabItem не приблизили к желаемой цели. Оказалось что эти события происходят слишком рано и после этого UserControl получает массу сообщений и в результате теряет фокус. Тоже самое касается и события Activated окна WPF.

    Потратив кучу времени на поиск решения я не нашел ничего кроме констатации факта существования данной проблемы. Но решение все же нашлось. Решение ЖУТКОЕ, крайне ненадежное, но все же в 99% случаев приводящее к желаемому результату – отложить установку фокуса в UserControl на некоторое время.

    private void SetFocuToWindowsForms()
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(delegate(object o)
        {
                    //Ждем пока WPF проделает
            //все что хочет с нашим UserControl

                    Thread.Sleep(100);
            windowsFormsControl.Invoke(new Func<bool>(windowsFormsControl.Focus));
        }));
    }

    private void WpfWindow_Activated(object sender, EventArgs e)
    {
        if (wfTab.IsSelected)
        {
            SetFocuToWindowsForms();
        }
    }

    private void windowsFormsTab_Selected(object sender, RoutedEventArgs e)
    {
        SetFocuToWindowsForms();
    }

    Повторюсь еще раз, что этот код может не работать в некоторых ситуация, но другого решения найти пока не удалось.

    Надіслане 30-10-2009 02:14 від Dmitry Peleshenko | 2 коментарів
    Зареєстрований як
  • Автоматизированное построение интерпретаторов с помощью ANTLR 3

        Недавно возникла проблема, необходимо было разработать небольшой интерпретатор. Причем необходимо было использовать его в проекте на .NET Framework и интерпретатор должен полностью состоять из управляемого кода, т.к. необходимо использовать CAS. Первое что приходит в голову в таких случаях это старый добрый bison. Давно проверенное и привычное средство автоматизированного построения парсеров, интерпретаторов и т.д. Первым делом я попытался найти аналог bison для C#. Действительно существует такое средство, называется Gardens Point Parser Generator (gppg). Инструмент не совсем поддерживает формат грамматики для  bison и содержит отличия связанные с использованием .NET Framework и что самое главное,  gppg ограничивает правую часть правила всего восемью членами. Конечно можно постараться преобразовать грамматику  к такому виду, но это создает определенные неудобства, необходимо создавать дополнительные нетерминальные символы и т.д.
    Просмотрев ряд других средств для построения интерпретаторов, я остановился на инструменте под названием ANother Tool for Language Recognition (ANTLR).
        Как и bison ANTLR принимает на вход грамматику и генерирует на выходе синтаксический анализатор реализованный на целевом языке. ANTLR способен строить анализаторы для LL(*) грамматик – расширения LL(k) грамматик. Это дает большую свободу при  описании языков в сравнении с LALR(1) грамматиками bison. Не вдаваясь в теоретические подробности рассмотрим следующую грамматику:
       
        S::={ A | B }*
        A::=a | a b c
        B::=b d


    и следующую цепочку, являющуюся одним из порождений этой грамматики:
       
        abd

        После прочтения терминала a и обнаружения терминала b в look-ahead буфере единичной длинны невозможно решить, является ли ab началом последовательности abc, которую можно свернуть по правилу A::=abc или же эту последовательность нужно сворачивать по правилам A::=a и B::=b d. Для этого решения необходима информация о терминале, следующем за b в рассматриваемой последовательности, т.е. буфер просмотра вперед должен содержать два терминала, а не один. Размер этого буфера у анализатора, сгенерированного bison как раз равен единице – он предназначен для разбора LALR(1)-грамматик. В этом случае Вам придется преобразовывать свою грамматику. При использовании ANTLR никаких преобразований не требуется.
        Согласно описанию ANTLR, LL(*) грамматика – это LL(k) грамматика в которой k может неограниченно возрастать. Рассмотрим еще один пример:

    // "int f(int x,int y);"
    // "int f() {...}"
       method ::= type ID '(' args ')' ';'

             | type ID '(' args ')' '{' body '}'   

       type ::= 'void' | 'int'

    // "int x, int y, int z, ..."

       args ::= arg (',' arg)*                    

       arg  ::= 'int' ID
       body ::= ...
       
       
    Такая грамматика не является LL(k) грамматикой, т.к. неизвестно какой длинны должен быть look-ahead буфер, т.е. нельзя заранее определить значение k. Это происходит потому, что цепочка начинающаяся с type ID '(' далее может содержать неограниченное число конструкций arg, а для выбора альтернативы, в первом правеле, необходимо проанализировать последние 3 символа цепочки. Такая грамматика классифицируется как LL(*) и ANTLR начиная с версии 3 успешено справляется с такими грамматиками.
    Целевым языком для bison является С++, для gppg – С#, для ANTLR это могут быть Java, С++ или С#. Для того чтобы не просто определить принадлежит ли некоторая цепочка заданной грамматике а еще и выполнять какие-то действия в процессе разбора этой цепочки в ANTLR как и в bison в каждом правиле задаются эти самые действия выраженные на целевом языке. Действия задаются для каждой альтернативы правила. И здесь снова прослеживается ряд приятных отличий от  bison:
    • существует блок init выполняемый перед кодом из любой альтернативы;
    • правила могут иметь параметры;
    • обращение к символам правой части правила осуществляется не по номерам а с помощью идентификаторов.
        Создатели ANTLR утверждают, что многие приимущества при определении действий для правил являются следствием того, что ANTLR осуществляет LL рразбор, т.е. использует разбор сверху вниз, в отличии от bison и gppg, которые используют разбор снизу вверх.
        Кроме того ANTLR выгодно отличается от других наличием визуальной среды разработки ANTLRWorks, позволяющей создавать и отлаживать грамматики.




        Здесь приведены далеко не все возможности ANTLR, есть также возможность построения деревьев разбора, использовани синтаксических и сематических предикатов, которые позволяют разработчику вмешаться в процесс разбора, и многое другое.
  • Наблюдение за кешированием

      Изучая ASP.NET я недавно наткнулся на определенные трудности с кэшированием страниц на прокси серверах. Задавал даже вопрос на форуме. Спасибо 2 Mike Chaliy предложенное решение вполне работоспособно. Но я нашел еще кое что на эту тему. Причем все обнаруженные мной особенности никак не проявляли себя при тестировании на локальной машине и в локальной сети.
      На своей домашней машине я установил IIS и разработал небольшое ASP.NET приложение для управления файлами на сервере и для отправки сообщений на сервер. Все работало замечательно на локальной машине во время отладки. И даже из локальной сети все тоже выглядело превосходно. На следующий день, воспользовавшись всеми прелестями ADSL от Укртелеком и DynamicDNS, я отправился в ХИРЭ "поуправлять" своей машиной удаленно. Выход в инет там организован через прокси, да еще и на кафедре стоит свой маленький прокси. С успехом загрузилась первая страничка, но потом все перестало работать, запросы до сервера не доходили а ответы грузились из кеша прокси, вобщем работать было невозможно. Нажимая сабмит я получал все ту же страничку. Псоле вопроса на форуме я вставил
        
        Response.Cache.SetCacheability(HttpCacheability.NoCache);

    во все веб-формы в обработчик Page_Load. Желаемый результат был достигнут. Мне показалась странной необходимость вставлять эту строчку. Почитав документацию по ASP.NET я нашел там такую вот директиву

        <%@ OutputCache Duration="1" VaryByParam="None" %>

      Размещаете ее на каждой странице и все работает нормально, Duration задается в секундах. Есть еще куча параметров по настройке кеширования. Кешировать, оказывается, можно на сервере, на прокси или клиенте и исключительно на клиенте. Причем директива применима как к странице целиком, так и к UserControl.
      Порывшись в настройках IIS нашел там в разделе заголовков HTTP настройки срока действия содержимого и тут же установил настройку "истекает немедленно". Убрал все дополнительные настройки кэширования с веб-форм. Проверил все работает отлично, с одним только недостатком. Кнопочки на своей страничке я приукрасил повесив на них картинку и на onmouseover и onmouseout соответсвующие Java-скрипты меняющие картинку. Картинка естественно грузилась с моего сервера и при каждом срабатывании скрипта, а не при первом, кнопка становилась черной а потом загружалась соответствующая картинка, т.к. картинка подобно всему содержимому не кэшировалась браузером.  Вобщем отвратительно выглядело. В итоге я решил вернутся к варианту №2 указав соответствующие директивы в каждой форме и изменив настройку IIS на однодневный срок действия содержимого. Этот вариант работал наиболее приемлемо.
      Самое интересное что на локальной машине и в локальной сети ничего не кэшировалось вообще и все выглядело так, будто эти настройки не дают вообще никакого эффекта, хотя я и не протестировал все возможные параметры @OutputCache.
      Приложение было очень простым и то вызывало недоумение первые несколько секунд, представляю что будет с более сложными если не позаботится о правильном кэшировании.

Календар повідомлень

<February 2010>
SMTWTFS
31123456
78910111213
14151617181920
21222324252627
28123456
78910111213

Пошук

Go

Синдикація

SkinName:iroha_Blog2