вторник, 15 января 2013 г.

WCF DataServices - расширение клиентской модели


   При разработке многоуровневых  приложений с использование WCF + OData сервисов зачастую возникает необходимость расширить сущностные классы на клиенте, т.е. добавить свойство, которого нет в серверной EDMX модели.
   Например вот так:
public partial class Enterprise : global::System.ComponentModel.INotifyPropertyChanged
{
   public string NewProperty
   {
      get
      {
            …
      }
   }
}
   Однако, добавив новое свойство, мы получаем ошибку времени выполнения связанную с невозможностью сериализации вновь добавленного члена класса. Дело в том, что при сериализации и отправке запроса на сервер выполняется сверка XML описания объектов, соответственно, т.к. свойство добавлено только на клиенте генерируется исключение. Решение проблемы - пометить свойство как не подлежащее сериализации. Для этого потребуется создать атрибут, которым будем помечать свойства, расширяющие клиентскую модель и необходимо реализовать метод который должен быть подписан на событие сериализации сущности (DataServiceContext.WritingEntity). Задачей метода является удаление свойств, помеченных атрибутом запрета сериализации,  из XML схемы описания объекта. Синтаксис метода ниже:
void DataServiceContextEx_WritingEntity
      (object sender, ReadingWritingEntityEventArgs e)
 {
   // e.Data - сериализуемый элемент
xnEntityProperties = XName.Get("properties"
                    ,e.Data.GetNamespaceOfPrefix("m").NamespaceName);
XElement xePayload = null;
foreach (PropertyInfo property in e.Entity.GetType().GetProperties())
   {
       object[] doNotSerializeAttributes =    
                property.GetCustomAttributes(typeof(DoNotSerializeAttribute), false); 
       if (doNotSerializeAttributes.Length > 0)
       {
         if (xePayload == null)
         {
               xePayload = e.Data
                            .Descendants()
                            .Where<XElement>(xe => xe.Name == xnEntityProperties)
                            .First<XElement>();
         } 
         XName xnProperty = XName.Get(property.Name,                     
                                      e.Data.GetNamespaceOfPrefix("d").NamespaceName);
         foreach (XElement xeRemoveThisProperty in 
                        xePayload.Descendants(xnProperty).ToList())
         {
            xeRemoveThisProperty.Remove();
         }
       }
    }
  }

Ну а синтаксис атрибута совсем простой:
[
AttributeUsage(AttributeTargets.Property)]
public class DoNotSerializeAttribute : Attribute
{
}
Вот и всё!

пятница, 9 ноября 2012 г.

Подсказки в WPF компонентах - TextBox и ComboBox

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


при этом не изменить обычного поведения TextBox и ComboBox.
   Начнем, пожалуй,  с простого  – реализуем поведение вспомогательной информационной подсказки для  TextBox. Требования к поведению – при получении фокуса ввода, либо в случае если свойству Text соответствует не пустое значение, подсказка должна пропадать. Очевидно, что свойство Text для отображения подсказки использовать нельзя, иначе компонент не позволит ввести пустую строку.  Помимо этого у TextBox нет стандартного поля для хранения строки содержащей подсказку.
   Пожалуй самый простой способ добавить поле в компонент – прибегнуть к присоединяемым свойствам. Класс, содержащий свойство для хранения подсказки будет иметь следующий вид:

public sealed class WaterMarkExtentions
    {
        public static string GetWaterMark(DependencyObject obj)
        {
            return (string)obj.GetValue(WaterMarkProperty);
        }

        public static void SetWaterMark(DependencyObject obj, string value)
        {
            obj.SetValue(WaterMarkProperty, value);
        }

        public static readonly DependencyProperty WaterMarkProperty =
           DependencyProperty.RegisterAttached("WaterMark"
                                       ,typeof(string)
                                       ,typeof(FrameworkElement)
                                       ,new FrameworkPropertyMetadata(""));
    }

   Далее целесообразно воспользоваться триггерами для того чтобы управлять отображением подсказки, соответственно необходимо описать стиль. А вот и он:

<Style TargetType="TextBox" x:Key="WaterMarkTextboxStyle">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="TextBox">
               <Border Background="{TemplateBinding Background}" 
                       BorderBrush="{TemplateBinding BorderBrush}"
                       BorderThickness="{TemplateBinding BorderThickness}" >
                   <Grid>
                       <ScrollViewer x:Name="PART_ContentHost" />
                       <TextBlock x:Name="WatermarkText"
                          Text="{Binding WaterMark, 
                              RelativeSource={RelativeSource TemplatedParent}}"
                          Foreground="Gray" Margin="5,0,0,0" 
                          HorizontalAlignment="Left" 
                          VerticalAlignment="Center" 
                          Visibility="Collapsed" 
                          IsHitTestVisible="False"/>
                   </Grid>
               </Border>
               <ControlTemplate.Triggers>
                   <MultiTrigger>
                      <MultiTrigger.Conditions>
                        <Condition Property="IsKeyboardFocusWithin" 
                                   Value="False"/>
                        <Condition Property="Text" Value=""/>
                      </MultiTrigger.Conditions>
                   <Setter Property="Visibility" 
                           TargetName="WatermarkText" 
                           Value="Visible"/>
                   </MultiTrigger>
                   <MultiTrigger>
                      <MultiTrigger.Conditions>
                        <Condition Property="IsKeyboardFocusWithin" 
                                   Value="False"/>
                        <Condition Property="Text" Value="{x:Null}"/>
                       </MultiTrigger.Conditions>
                   <Setter Property="Visibility"
                           TargetName="WatermarkText" 
                           Value="Visible"/>
                   </MultiTrigger>
               </ControlTemplate.Triggers>
           </ControlTemplate>
         </Setter.Value>
       </Setter>
    </Style>

   Подсказка отображается в TextBlock (WatermarkText), который располагается над контентом нашего TextBox. Свойством Visibility у  WatermarkText управляют триггеры в зависимости от того есть ли фокус ввода у компонента, и от значения свойства Text.
Собственно все, компонент готов к использованию. Ниже пример использования:

<Window x:Class="WaterMarkStyle.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:System="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:WaterMarkStyle"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
     <TextBox 
        local:WaterMarkExtentions.WaterMark = "sample textbox watermark text"
        Style="{StaticResource WaterMarkTextboxStyle}"    
        HorizontalAlignment="Left"/>
    </Grid>
</Window>
 
   Далее, как и обещал в начале, тот же фокус выполним для ComboBox. Идея, в общем, та же, но вот шаблон компонента значительно сложнее, его можно найти на msdn. Теперь все что осталось – это переработать описание стиля шаблона для поля ввода, которое называется –  PART_EditableTextBox. Стиль описанный ранее (WaterMarkTextboxStyle) великолепно сюда подходит, однако, в виду сложности шаблона для ComboBox, придется поправить Binding подсказки следующим образом:

  <TextBlock x:Name="WatermarkText" 
Text="{Binding WaterMark, RelativeSource=
       {RelativeSource Mode=FindAncestor, AncestorType=ComboBox}}"
Foreground="Gray" Margin="5,0,0,0" 
HorizontalAlignment="Left" VerticalAlignment="Center"
Visibility="Collapsed" IsHitTestVisible="False"/>

   Ну и последний штрих – теперь за отображение подсказки будет отвечать еще и выбранный в выпадающем списке элемент. Помимо этого надо не забывать про флаг IsEditable. Поэтому добавим в триггеры шаблона ComboBox:

<ControlTemplate.Triggers>
...
<MultiTrigger>
      <MultiTrigger.Conditions>
           <Condition Property="IsEditable" Value="false"/>
           <Condition Property="SelectedItem" Value="{x:Null}"/>
      </MultiTrigger.Conditions>
   <Setter TargetName="PART_EditableTextBox" 
           Property="Visibility"   
           Value="Visible"/>
</MultiTrigger>

   Пример использования будет абсолютно аналогичный:

  <ComboBox 
        local:WaterMarkExtentions.WaterMark = "sample combobox watermark text"
         Style="{StaticResource WaterMarkComboBoxStyle}" >
     <System:String>test1</System:String>
     <System:String>test2</System:String>
 </ComboBox>