четверг, 8 апреля 2010 г.

WPF, DataBinding и ToolTip

WPF - интересная технология! Интересная, прежде всего, своими странностями и непонятностями, которые, вкупе с мощью и развесистостью библиотек, не перестают удивлять. :о)) В этот раз "порадовал" датабаиндинг внутри комплексного тултипа (всплывающей подсказки). Примитивнейший, казалось бы, пример:

<Window
  x:Class="WpfApplication2.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:WpfApplication2"
  Width="300" Height="300"
>
  <Window.Resources>
    <XmlDataProvider x:Key="MyData" XPath="//test:Items/test:Item">
      <XmlDataProvider.XmlNamespaceManager>
        <XmlNamespaceMappingCollection>
          <XmlNamespaceMapping Uri="urn:test" Prefix="test" />
        </XmlNamespaceMappingCollection>
      </XmlDataProvider.XmlNamespaceManager>
      <x:XData>
        <Items xmlns="urn:test">
          <Item Text="Text 1" Description="Description 1" />
          <Item Text="Text 2" Description="Description 2" />
          <Item Text="Text 3" Description="Description 3" />
        </Items>
      </x:XData>
    </XmlDataProvider>
 
    <DataTemplate x:Key="MyTemplate">
      <TextBlock Text="{Binding XPath='@Text'}">
        <TextBlock.ToolTip>
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding XPath='@Text'}" />
            <TextBlock Text=" : " />
            <TextBlock Text="{Binding XPath='@Description'}" />
          </StackPanel>
        </TextBlock.ToolTip>
      </TextBlock>
    </DataTemplate>
  </Window.Resources>
 
  <ItemsControl ItemTemplate="{StaticResource MyTemplate}" ItemsSource="{Binding Source={StaticResource MyData}}" />
</Window>

Но если включить вывод отладочной информации из источника System.Windows.Data (см. Trace sources in WPF или недавний пост про Отключение PresentationTraceSources в WPF) то в окошке Output студии можно видеть:

System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:XPath=@Description; DataItem=null; target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')
System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:XPath=@Text; DataItem=null; target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')
System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:XPath=@Description; DataItem=null; target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')
System.Windows.Data Information: 40 : BindingExpression path error: 'InnerText' property not found for 'current item of collection' because data item is null. This could happen because the data provider has not produced any data yet. BindingExpression:Path=/InnerText; DataItem=null; target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')
System.Windows.Data Information: 19 : BindingExpression cannot retrieve value due to missing information. BindingExpression:Path=/InnerText; DataItem=null; target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')
System.Windows.Data Information: 20 : BindingExpression cannot retrieve value from null data item. This could happen when binding is detached or when binding to a Nullable type that has no value. BindingExpression:Path=/InnerText; DataItem=null; target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')
System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:Path=/InnerText; DataItem=null; target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')
System.Windows.Data Information: 40 : BindingExpression path error: 'InnerText' property not found for 'current item of collection' because data item is null. This could happen because the data provider has not produced any data yet. BindingExpression:Path=/InnerText; DataItem=null; target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')

Мне это кажется непорядком: а почему бы и не выполнить предписания и не сделать баиндинг только тогда, когда данные действительно появятся? При этом получилось вот что:

<DataTemplate x:Key="MyTemplate">
  <TextBlock Text="{Binding XPath='@Text'}">
    <TextBlock.ToolTip>
      <ToolTip>
        <StackPanel Name="ToolTip" Orientation="Horizontal">
          <TextBlock>
            <TextBlock.Style>
              <Style TargetType="{x:Type TextBlock}">
                <Setter Property="Text" Value="{Binding XPath='@Text'}" />
                <Style.Triggers>
                  <Trigger Property="DataContext" Value="{x:Null}">
                    <Setter Property="Text" Value="" />
                  </Trigger>
                </Style.Triggers>
              </Style>
            </TextBlock.Style>
          </TextBlock>
          <TextBlock Text=" : " />
          <TextBlock>
            <TextBlock.Style>
              <Style TargetType="{x:Type TextBlock}">
                <Setter Property="Text" Value="{Binding XPath='@Description'}" />
                <Style.Triggers>
                  <Trigger Property="DataContext" Value="{x:Null}">
                    <Setter Property="Text" Value="" />
                  </Trigger>
                </Style.Triggers>
              </Style>
            </TextBlock.Style>
          </TextBlock>
        </StackPanel>
      </ToolTip>
    </TextBlock.ToolTip>
  </TextBlock>
</DataTemplate>

То есть баиндинг выставляется через стиль, а затем в DataTrigger-е сбрасывается в случае, когда данных нет.

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

пятница, 2 апреля 2010 г.

Сниппеты для проверки аргументов на null

Думаю, есть время поделиться моими любимыми и наиболее часто используемыми сниппетами.

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

Не секрет, что для упрощения проверок аргументов на null изобретено не мало средств: от использования возможностей языка\компилятора до специальных инструментов a-la CodeContracts, аннотоций ReSharper-а или колдунства PostSharp-а.

Мне всё это (пока что) кажется излишним оверхедом: тратить время на разбор выражений обидно, с контрактами действительно пока не всё понятно, завязываться же на инструмент третий стороны не хочется вовсе.

Итак, как делаю я: написав объявление метода и операторные скобки к нему, обозначив тело:

void MyMethod(object param1, object param2) {
  | // <- это позиция курсора в редакторе
}

…набираю "an" (это shortcut для сниппета), жму Tab и получаю:


if(ArgName == null) {
  throw new ArgumentNullException("ArgName");
}//if

Осталось ввести имя аргумента (курсор уже там, где нужно, в выделенном квадрате + помогает IntelliSense) и нажать Enter:

void MyMethod(object param1, object param2) {
  if(param1 == null) {
    throw new ArgumentNullException("param1");
  }//if
  |
}

Можно приступать к написанию тела метода.

Но если нужно проверить и второй параметр, то ставлю курсор перед закрывающей скобкой: }//if:

void MyMethod(object param1, object param2) {
  if(param1 == null) {
    throw new ArgumentNullException("param1");
 |}//if
}

…набираю "an2", жму Tab и получаю:

void MyMethod(object param1, object param2) {
  if(param1 == null) {
    throw new ArgumentNullException("param1");
  } else if(ArgName == null) {
    throw new ArgumentNullException("ArgName");
  }//if
}

Опять быстро с помощью IntelliSense ввожу имя второго параметра, жму Enter:

void MyMethod(object param1, object param2) {
  if(param1 == null) {
    throw new ArgumentNullException("param1");
  } else if(param2 == null) {
    throw new ArgumentNullException("param2");
  }//if
  |
}

Вот так вот несколькими нажатиями я "набираю" довольно много нужного кода.

В добавок к сниппетам "an" и "an2" у меня есть сниппеты ("ans" и "ans2" соответственно) для проверки строк, которые отличаются от первых тем, что вместо проверки на null в условии выполняется проверка аргумента с помощью String.IsNullOrEmpty(string).

Выглядит первый ("an") сниппет так:

<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Title>Throws ArgumentNullException</Title>
      <Shortcut>an</Shortcut>
      <Description>Code snippet for throws ArgumentNullException</Description>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
      <Author>Viacheslav.Ivanov@GMail.com</Author>
    </Header>
    <Snippet>
      <Declarations>
        <Literal>
          <ID>ArgumentName</ID>
          <ToolTip>Name of the argument</ToolTip>
          <Default>ArgName</Default>
        </Literal>
        <Literal Editable="false">
          <ID>ArgumentNullException</ID>
          <Function>SimpleTypeName(global::System.ArgumentNullException)</Function>
          <ToolTip>Type of the exception</ToolTip>
        </Literal>
      </Declarations>
      <Code Language="CSharp">
<![CDATA[if($ArgumentName$ == null) {
      throw new $ArgumentNullException$("$ArgumentName$");
    }//if
    $end$]]>
      </Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

Остальные сниппеты такие (привожу здесь только "значимую" часть, упустив заголовок):

Проверка второго и последующих аргументов на null ("an2"):

<Code Language="CSharp">
<![CDATA[} else if($ArgumentName$ == null) {
      throw new $ArgumentNullException$("$ArgumentName$");$end$]]>
</Code>

Проверка строки на IsNullOrEmpty ("ans"):

<Snippet>
  <Declarations>
    <Literal>
      <ID>ArgumentName</ID>
      <ToolTip>Name of the argument</ToolTip>
      <Default>ArgName</Default>
    </Literal>
    <Literal Editable="false">
      <ID>String</ID>
      <Function>SimpleTypeName(global::System.String)</Function>
      <ToolTip>System.String type</ToolTip>
    </Literal>
    <Literal Editable="false">
      <ID>ArgumentNullException</ID>
      <Function>SimpleTypeName(global::System.ArgumentNullException)</Function>
      <ToolTip>Type of the exception</ToolTip>
    </Literal>
  </Declarations>
  <Code Language="CSharp">
<![CDATA[if($String$.IsNullOrEmpty($ArgumentName$)) {
      throw new $ArgumentNullException$("$ArgumentName$");
    }//if
    $end$]]>
  </Code>
</Snippet>

Проверка второго и последующий аргументов строкового типа на IsNullOrEmpty ("ans2"):

<Code Language="CSharp">
<![CDATA[} else if($String$.IsNullOrEmpty($ArgumentName$)) {
      throw new $ArgumentNullException$("$ArgumentName$");$end$]]>
</Code>

Скачать готовые сниппеты можно отсюда. О том, как их подключить к MSVS можно прочитать в статье How to: Manage Code Snippets.

четверг, 11 марта 2010 г.

Возрождение :о))

Ура!

Мне наконец-то удалось найти удобный способ написания сообщений в свой же блог, а это была единственная причина, останавливавшая меня от приступов графоманства :о) Мне удалось достаточно быстро немного переформатировтаь предыдущие посты (прошу прощение, если кому-то в rss помешали мои всплывшие сообщения) и шустро написать один новый.

Надеюсь, полосы прокрутки для кода никому сильно не помешают, мне показалось что они удобнее, чем перенос строк.

Если кто знает, как имеющийся у меня шаблон внешнего вида блога настроить так, что бы центральная панель с сообщениями была бы не фиксированного значения, а занимала бы всё доступное (по ширине) пространство - прошу помочь. Сильно хочу так сделать, но пока не получается. У кого-то случайно видел такой шаблон, но уже не могу вспомнить у кого именно :о(

Кому интересно, то технология у меня простая - создаю в MSVS (2010) новую HTML страничку и в ней набираю то, что хочу опубликовать. Сложно было разобраться с переносами - наконец-то нашёл, где тут переключатели, позволяющие не реагировать текстовому процессору на обычный перевод строки и обращать внимание только на соответствующие явные средства HTML a-la <br /> и <p />. Есть один общий плюс к каждому посту свой собственный такой переключатель.

Весь код и его раскраску набираю сам врукопашную же в том же текстовом редакторе студии. Оказывается, это совсем не сложно :о)) Xml набирать сложнее :о)) Ни одного достаточно удобного плагина к студии на эту тему отыскать не сумел.

Зато выглядит теперь всё так, как мне нравится, а, значит, развивать сие будет интереснее.

Отключение PresentationTraceSources в WPF

Если вы когда-либо отлаживали WPF-приложение, то могли видеть в окошке Output отладчика примерно такой вывод:

System.Windows.Data Error: 4 : Cannot find source for binding with reference …
System.Windows.Data Error: 39 : BindingExpression path error: …

и тому подобное. Это "работает" класс PresentationTraceSources. Подробнее о нём можно узнать в статьях Trace sources in WPF и How can I debug WPF bindings?.

Я расскажу не о том, зачем нужен этот класс и не о том, как им пользоваться, а о том, как же его "отключить", то есть как добиться того, что бы в Output не писалось то, что вам, может быть, и не нужно.

Самый простой способ отключения вывода PresentationTraceSources - програмный:

Action<TraceSource> disable = traceSource => traceSource.Switch.Level = SourceLevels.Off;
disable(PresentationTraceSources.AnimationSource);
disable(PresentationTraceSources.DataBindingSource);
disable(PresentationTraceSources.DependencyPropertySource);
// … И так далее по всем имеющимся TraceSource-ам

Но этот способ и самый неинтересный, потому что его трудно поддерживать: необходимо изменить код (и, как следствие - перекомпилировать программу) что бы добавить, убрать или как-то более хитро настроить полезный зачастую вывод.

Правильный путь: воспользоваться файлом конфигурации. Но и следующее решение не будет работать:

<configuration>
  <system.diagnostics>
    <sources>
      <source name="System.Windows.Data" switchValue="Off" />
      <!-- и так далее … -->
    </sources>
  </system.diagnostics>
</configuration>

при запуске программы из-под отладчика, из-за того, что внутри PresentationTraceSources при создании экземпляра TraceSource проверяется, не подключён ли отладчик, и если подключён, и для switchValue указано значение Off, то будет использоваться значение SourceLevels.Warning.

Зная вышесказанное, не сложно исхотриться так:

<source name="System.Windows.Data" switchValue="Critical" />
<!-- и так далее … -->

где Critical - самый строгий уровень вывода, но отличный от Off. На практике ни одного сообщения с критическим уровнем важности я ещё не встечал, поэтому в ситуациях, когда хочется по возможности максимально избавиться от ненужного вывода, можно воспользоваться описанным советом.

На последок, полный пример с небольшой универсализацией, позволяющей задавать уровень вывода PresentationTraceSources один раз:

<configuration>
  <system.diagnostics>
    <sources>
      <source name="System.Windows.Data" switchName="PresentationTraceSwitch" />
      <source name="System.Windows.DependencyProperty" switchName="PresentationTraceSwitch" />
      <source name="System.Windows.Documents" switchName="PresentationTraceSwitch" />
      <source name="System.Windows.Freezable" switchName="PresentationTraceSwitch" />
      <source name="System.Windows.Interop.HwndHost" switchName="PresentationTraceSwitch" />
      <source name="System.Windows.Markup" switchName="PresentationTraceSwitch" />
      <source name="System.Windows.Media.Animation" switchName="PresentationTraceSwitch" />
      <source name="System.Windows.NameScope" switchName="PresentationTraceSwitch" />
      <source name="System.Windows.ResourceDictionary" switchName="PresentationTraceSwitch" />
      <source name="System.Windows.RoutedEvent" switchName="PresentationTraceSwitch" />
      <!-- Для 3.5 указано всё, что есть -->
    </sources>
    <switches>
      <!-- Do not use an "Off", because under debugger it's replaced to "Warning". -->
      <add name="PresentationTraceSwitch" value="Critical" />
    </switches>
  </system.diagnostics>
</configuration>

И не забудьте где-либо в коде вашей программы вызвать

PresentationTraceSources.Refresh();

без вызова метода Refresh() значения не будут зачитаны из конфигурационного файла.