четверг, 26 августа 2010 г.

Silverlight и сортировка по вычисляемому полю.

Сейчас мы рассмотрим способ, при помощи которого можно делать сортировку в DataGrid для вычисляемых полей.

Предположим, что в нашей таблице есть поле DateLoaded, которое отвечает за дату загрузки некоторого ряда данных в таблицу. Дата задана с стандартном формате, но нам бы хотелось показывать вместо просто даты квартал и год в таблице DataGrid. Сделать это не сложно. Для этого добавим новый расшаренный класс SomeTable.shared.cs, в котором опишем данное вычисляемое свойство:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Runtime.Serialization;

using System.ComponentModel.DataAnnotations;
 

namespace MyApplication.BLL

{
     public partial class SomeTable
     {


        public string Quarter
         {
             get
             {       int q = (DateLoaded.Value.Month - 1) / 3 + 1;
                     return string.Format("Q{0} {1}", q, DateLoaded.Value.Year);            }
             set
             {
                 return;
             }
         }
     }

}


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



<navigation:Page x:Class="MyApplication.Views.Somes" 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            mc:Ignorable="d"
            xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
            d:DesignWidth="640" d:DesignHeight="480"
            Style="{StaticResource PageStyle}"
                  xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.DomainServices"
                  xmlns:my="clr-namespace:MyApplication.Web.Services"
                  xmlns:my1="clr-namespace:MyApplication.BLL"
                  xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
                  xmlns:my2="clr-namespace:MyApplication.Controls"

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

xmlns:behaviors="clr-namespace:MyApplication.Helpers;assembly=MyApplication"

xmlns:converter="clr-namespace:MyApplication"

>
     <navigation:Page.Resources>
         <converter:VisibilityConverter x:Key="VisibilityConverter" />
     </navigation:Page.Resources>
     <Grid x:Name="LayoutRoot">
         <ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}" >
 
             <StackPanel x:Name="ContentStackPanel" Style="{StaticResource ContentStackPanelStyle}">
 
                 <TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"
                            Text="{Binding Path=ApplicationStrings.SomesPageTitle, Source={StaticResource ResourceWrapper}}"/>


bility="Collapsed">
                     <StackPanel Orientation="Horizontal">
                         <TextBlock Text="Project:" VerticalAlignment="Center"/>
                         <ComboBox HorizontalAlignment="Left" Height="23" ItemsSource="{Binding ElementName=projectDomainDataSource, Path=Data}" DisplayMemberPath="Name" Name="projectComboBox" Width="120" Margin="10,0,0,0" SelectionChanged="projectComboBox_SelectionChanged">
                             <ComboBox.ItemsPanel>
                                 <ItemsPanelTemplate>
                                     <VirtualizingStackPanel />
                                 </ItemsPanelTemplate>
                             </ComboBox.ItemsPanel>
                         </ComboBox>
 
                     </StackPanel>



                 <riaControls:DomainDataSource AutoLoad="True" d:DesignData="{d:DesignInstance my1:Some, CreateList=true}" Height="0" LoadedData="someDomainDataSource_LoadedData" Name="someDomainDataSource" QueryName="GetSomesQuery" Width="0" >
                     <riaControls:DomainDataSource.DomainContext>
                         <my:MyApplicationDomainContext />
                     </riaControls:DomainDataSource.DomainContext>
                     <riaControls:DomainDataSource.SortDescriptors>
                         <riaControls:SortDescriptor PropertyPath="ID" Direction="Ascending"/>
                     </riaControls:DomainDataSource.SortDescriptors>
                     <riaControls:DomainDataSource.QueryParameters>
                         <riaControls:Parameter ParameterName="projectId" Value="{Binding ElementName=projectComboBox, Path=SelectedItem.ID}" />


                    </riaControls:DomainDataSource.QueryParameters>
 
                 </riaControls:DomainDataSource>
                 <my2:BusyIndicator x:Name="busyIndicator1" IsBusy="{Binding ElementName=someDomainDataSource, Path=DomainContext.IsLoading}" BusyContent="Loading..." >
 
                     <sdk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding ElementName=someDomainDataSource, Path=Data}" Name="someDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" ColumnWidth="Auto" IsReadOnly="True" SelectionMode="Single" SizeChanged="someDataGrid_SizeChanged">
                         <sdk:DataGrid.Columns>
                             <sdk:DataGridTextColumn x:Name="quaterColumn" Binding="{Binding Path=Quarter}" Header="Quarter" MinWidth="10"/>


 // дальше должно следовать описание других колонок


 
                         </sdk:DataGrid.Columns>
                         <i:Interaction.Behaviors>
 
                             <behaviors:DataGridDoubleClickBehavior DoubleClick="someDataGrid_DoubleClick"/>
 
                         </i:Interaction.Behaviors>
                     </sdk:DataGrid>
                 </my2:BusyIndicator>
                 <sdk:DataPager x:Name="somePager" PageSize="30" Source="{Binding ElementName=someDomainDataSource, Path=Data}">
                 </sdk:DataPager>
                 <riaControls:DomainDataSource AutoLoad="True" d:DesignData="{d:DesignInstance my1:Project, CreateList=true}" Height="0" LoadedData="projectDomainDataSource_LoadedData" Name="projectDomainDataSource" QueryName="GetProjectsQuery" Width="0" >
                     <riaControls:DomainDataSource.DomainContext>
                         <my:MyApplicationDomainContext />
                     </riaControls:DomainDataSource.DomainContext>
                 </riaControls:DomainDataSource>
              </StackPanel>
         </ScrollViewer>
     </Grid>

</navigation:Page>


Сейчас у нас должен отображаться грид с данными о дате загрузки данных причем в нужном нам формате.



Теперь оказывается, что данные поле отображается корректно, но по нему нельзя отсортировать наш грид. Дело в том, что Ria Services в связке с Entity Framework пытается сделать запрос к базе данных, считая, что Quarter – это одно из полей таблицы.



В принципе тут существуют два подхода для разных случаев:



1. Создать представление (View) в базе данных, а в нем создать нужное вычисляемое поле. В таком случае запросы будут работать.



2. Каждый раз выгружать данные из базы и работать с ними уже «отсоединенно». Имеется в виду, что сейчас мы работаем обычно с колекциями IQueryable, что дает возможность не выгружать постоянно все данные (например, в случае когда нужно загрузить данные только для первой страницы грида). С другой стороны, нужно постоянно гонять данные по сети (при каждой сортировке, подгруке данных и т.д.), что также может плохо сказаться на производительности.



Выбирать Вам, но мне проще выполнить второй вариант.



Сначала нужно изменить код сервиса так, чтобы Get запрос возращал все данные. Для этого нужно просто вызвать метод ToList() и указать, что мы будем возращать IEnumerable вместо IQuearyable.



public IList<SomeTable> GetSomeTable(int projectId)
         {
             var user = this.ObjectContext.SomeTable.Where(p => p.projectId == projectId);
              return res.ToList();


         }


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



Теперь сортировка должна работать, но появляются еще несколько проблем:



1. Колонка Quartal таперь будет появляться во всех формах. Это легко решается аттрибутом Display.



2. В некоторых случаях (когда DateLoaded равно null) мы будем получать исключение в коде свойства Quartal. Придется выполнять дополнительную проверку.



3. Есть еще какая-то проблема, которой я не помню, но из-за нее пришлость добавить аттрибут DataMember…



Так что код будет примерно таким:



using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Runtime.Serialization;

using System.ComponentModel.DataAnnotations;
 

namespace MyApplication.BLL

{
     public partial class SomeTable
     {



         [DataMember]
         [Display(AutoGenerateField = false)]
         public string Quarter
         {
             get
             {
                 if (DateLoaded.HasValue)
                 {
                     int q = (DateLoaded.Value.Month - 1) / 3 + 1;
                     return string.Format("Q{0} {1}", q, DateLoaded.Value.Year);
                 }
                 return string.Empty;
             }
             set
             {
                 return;
             }
         }
     }

}


Еще одно интересное последствие того, что мы преобразовали запрос так, чтобы он возвращал List вместо IQueryable – это проблема с привязанными таблицами.



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



Раньше мы могли бы использовать свойство User.Name в колонке DataGrid. Теперь же оказывается, что при User == null мы получим исключение, чего и следовало ожидать при переходе к списку (грид пробует прочитать свойство User.Name в то время как User == null).



Решается эта проблема добавлением еще одного вычисляемого поля в наш файл:



        [DataMember]

        [Display(AutoGenerateField = false)]


public string UserName


        {


get


            {


if (User != null)


                {


return User.Name;


                }


return string.Empty;


            }


set


            {


return;


            }


        }



Код сервиса:

namespace MyApplication.Web.Services

{
     using System;
     using System.Collections.Generic;
     using System.ComponentModel;
     using System.ComponentModel.DataAnnotations;
     using System.Data;
     using System.Linq;
     using System.ServiceModel.DomainServices.EntityFramework;
     using System.ServiceModel.DomainServices.Hosting;
     using System.ServiceModel.DomainServices.Server;
     using MyApplication.BLL;


    using System.Data.Objects;
     using System.Text;
 
 
     // Implements application logic using the MyApplicationEntities context.
     // TODO: Add your application logic to these methods or in additional methods.
     // TODO: Wire up authentication (Windows/ASP.NET Forms) and uncomment the following to disable anonymous access
     // Also consider adding roles to restrict access as appropriate.
     // [RequiresAuthentication]
     [EnableClientAccess()]
     [RequiresAuthentication]
     public class MyApplicationDomainService : LinqToEntitiesDomainService<MyApplicationEntities>
     {


 


public IList<SomeTable> GetSomeTable(int projectId)
         {
             var user = this.ObjectContext.SomeTable.Where(p => p.projectId == projectId);
              return res.ToList();


         }


public void InsertSomeTable(SomeTable someTable)
         {
             if ((someTable.EntityState != EntityState.Detached))
             {
                 this.ObjectContext.ObjectStateManager.ChangeObjectState(someTable, EntityState.Added);
             }
             else
             {
                 this.ObjectContext.SomeTable.AddObject(someTable);
             }
         }
 
         public void UpdateSomeTable (SomeTable someTable)
         {
             this.ObjectContext.SomeTable.AttachAsModified(currentSome, this.ChangeSet.GetOriginal(someTable));
         }
 
         public void DeleteSomeTable(SomeTable someTable)
         {
             if ((someTable.EntityState == EntityState.Detached))
             {
                 this.ObjectContext.SomeTable.Attach(someTable);
             }
             this.ObjectContext.SomeTable.DeleteObject(someTable);
         }


 


}



}



Код SomeTable.shared.cs



using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Runtime.Serialization;

using System.ComponentModel.DataAnnotations;
 

namespace MyApplication.BLL

{
     public partial class SomeTable
     {


        [DataMember]
         [Display(AutoGenerateField = false)]
         public string UserName
         {
             get
             {
                 if (User != null)
                 {
                     return User.Name;
                 }
                 return string.Empty;
             }
             set
             {
                 return;
             }
         }
 
         [DataMember]
         [Display(AutoGenerateField = false)]
         public string Quarter
         {
             get
             {
                 if (DateLoaded.HasValue)
                 {
                     int q = (DateLoaded.Value.Month - 1) / 3 + 1;
                     return string.Format("Q{0} {1}", q, DateLoaded.Value.Year);
                 }
                 return string.Empty;
             }
             set
             {
                 return;
             }
         }
     }

}

Комментариев нет:

Отправить комментарий