Este artigo foi traduzido por máquina.

Fundações

Escrever ItemsControls mais eficiente

Charles Petzold

Download do código disponível na Galeria de código do MSDN
Procure o código on-line

Conteúdo

Uma plotagem de ItemsControl dispersão
A decepção de desempenho
Loops ocultos
Usando conversores de valor
Procurar sem Freezables!
Um apresentador intermediário
Elemento de dados personalizado
Biting the mais
A solução DrawingVisual

Há é uma hora a vida útil de cada programador do Windows Presentation Foundation (WPF) quando o verdadeiro poder do DataTemplate, de repente, fica evidente. Este epiphany geralmente é acompanhado por realização, " Ei, POSSO usar um DataTemplate para criar um gráfico de barras ou um dispersão plotagem com praticamente sem codificação. "

Um DataTemplate é mais comumente criada em conjunto com um ItemsControl ou uma classe que deriva de ItemsControl, que inclui a barra de ferramentas, barra de status de ListBox, ComboBox, menu, TreeView, — em curto, todos os controles que mantêm uma coleção de itens. O DataTemplate define como cada item na coleção é exibido. O DataTemplate consiste principalmente uma árvore visual de um ou mais elementos com ligações de dados que vinculam os itens na coleção com propriedades desses elementos. Se os itens na coleção implementar algum tipo de notificação de alteração de propriedade (geralmente implementando a interface de INotifyPropertyChanged), os itens-controle podem responder dinamicamente a alterações nos itens.

A decepção pode vir um pouco mais adiante. Se você precisar exibir grandes quantidades de dados, você poderá descobrir que ItemsControl e DataTemplate não dimensionar bem. Esta coluna é sobre o que você pode fazer para combater esses problemas de desempenho.

Uma plotagem de ItemsControl dispersão

Vamos criar um gráfico de dispersão de um ItemsControl e um DataTemplate. A primeira etapa é criar um objeto corporativo que representa o item de dados. a Figura 1 mostra uma classe simples com o nome genérico DataPoint (um pouco abridged). DataPoint implementa a interface INotify­PropertyChanged, que significa que ele contém um evento chamado PropertyChanged que o objeto é acionado sempre que uma propriedade é alterada.

A Figura 1 A classe DataPoint representa um item de dados

public class DataPoint : INotifyPropertyChanged
{
    int _type;
    double _variableX, _variableY;
    string _id;

    public event PropertyChangedEventHandler PropertyChanged;

    public int Type
    {
        set
        {
            if (_type != value)
            {
                _type = value;
                OnPropertyChanged("Type");
            }
        }
        get { return _type; }
    }

    public double VariableX [...]
    public double VariableY [...]
    public string ID [...]

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

As propriedades VariableX e VariableY indicam a posição do ponto em um sistema de coordenadas cartesianas. (Para esta coluna, o intervalo de valores de 0 a 1.) A propriedade de tipo pode ser usada para agrupar os pontos de dados (ela terão valores de 0 a 5 e ser usada para exibir informações em seis cores diferentes), e a propriedade ID identifica cada ponto com uma seqüência de caracteres de texto.

Em um aplicativo real, a seguinte classe DataCollection pode conter mais propriedades, mas para esse exemplo tem apenas um, DataPoints, do tipo ObservableCol­lection <datapoint>:

public class DataCollection
{
    public DataCollection(int numPoints)
    {
        DataPoints = new ObservableCollection<DataPoint>();
        new DataRandomizer<DataPoint>(DataPoints, numPoints, 
                                          Math.Min(1, numPoints / 100));
    }

    public ObservableCollection<DataPoint> DataPoints { set; get; }
}

ObservableCollection tem uma propriedade CollectionChanged que é acionada sempre que itens são adicionados ou removidos da coleção.

Essa classe DataCollection particular cria todos os itens de dados em seu construtor usando uma classe chamada DataPointRandomizer que gera dados aleatórios para fins de teste. O objeto DataPointRandomizer também define um timer. Cada décimo de segundo, o método de escala de timer altera a propriedade VariableX ou VariableY em 1 % dos pontos. Portanto, em média, todos os pontos de alterar cada 10 segundos.

Agora vamos escrever alguns XAML que exibe dados em um gráfico de dispersão. a Figura 2 mostra um UserControl que contém um ItemsControl. O DataContext deste controle será definido no código para um objeto do tipo DataCollection. A propriedade ItemsSource de ItemsControl é acoplado à propriedade DataPoints do DataCollection, o que significa que o ItemsControl será preenchido com os itens do tipo DataPoint.

Figura 2 O arquivo DataDisplay1Control.xaml

<UserControl x:Class="DataDisplay.DataDisplayControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:data="clr-namespace:DataLibrary;assembly=DataLibrary">

    <ItemsControl ItemsSource="{Binding DataPoints}">
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="data:DataPoint">
                <Path>
                    <Path.Data>
                        <EllipseGeometry RadiusX="0.003" RadiusY="0.003">
                            <EllipseGeometry.Transform>
                                <TranslateTransform X="{Binding VariableX}" 
                                                    Y="{Binding VariableY}" />
                            </EllipseGeometry.Transform>
                        </EllipseGeometry>
                    </Path.Data>

                    <Path.Style>
                        <Style TargetType="Path">
                            <Setter Property="Fill" Value="Red" />
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding Type}" Value="1">
                                    <Setter Property="Fill" Value="Yellow" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding Type}" Value="2">
                                    <Setter Property="Fill" Value="Green" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding Type}" Value="3">
                                    <Setter Property="Fill" Value="Cyan" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding Type}" Value="4">
                                    <Setter Property="Fill" Value="Blue" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding Type}" Value="5">
                                    <Setter Property="Fill" Value="Magenta" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Path.Style>

                    <Path.ToolTip>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding ID}" />
                            <TextBlock Text=", X=" />
                            <TextBlock Text="{Binding VariableX}" />
                            <TextBlock Text=", Y=" />
                            <TextBlock Text="{Binding VariableY}" />
                        </StackPanel>
                    </Path.ToolTip>
                </Path>
            </DataTemplate>
        </ItemsControl.ItemTemplate>

        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Grid Background="Azure" 
                      LayoutTransform="300 0 0 300 0 0" 
                      IsItemsHost="True" />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
</UserControl>

A propriedade ItemTemplate de ItemsControl é definida como um DataTemplate que define uma árvore visual para a exibição de cada item. Essa árvore consiste em apenas um elemento de caminho com sua propriedade de data definida como um EllipseGeometry. É realmente apenas um pouco ponto — e significará um ponto muito pouco — com um raio de unidades 0.003. Mas não é tão pequena quanto parece porque a propriedade ItemPanel ItemsControl está definida como um ItemsPanelTemplate que contém uma grade de célula única para exibir todos os pontos. Esta grade é fornecido um LayoutTransform que dimensiona-lo por um fator de 300, que torna a unidade de pontos quase 1 dados em radius.

Este manipulação ímpar é obrigatório, as ligações de dados das propriedades VariableX e VariableY às propriedades X e Y de um TranslateTransform. O VariableX e VariableY propriedades somente intervalo de 0 para 1, portanto, é necessário para aumentar o tamanho da grade ocupe uma área em quadrado de unidades de 300 a tela.

A propriedade de preenchimento do elemento Path é definida em um estilo para um pincel com base no valor da propriedade Type em cada objeto DataPoint. O objeto de caminho também é atribuído uma dica de ferramenta que exibe as informações sobre cada ponto de dados.

A decepção de desempenho

O código fonte que acompanha esta coluna consiste em uma solução do Visual Studio que contém sete projetos: seis aplicativos e uma biblioteca. A biblioteca denominada DataLibrary contém a maioria do código compartilhado, incluindo DataPoint, DataCollection e DataPointRandomizer.

O primeiro projeto de aplicativo é denominado DataDisplay1. Ele contém uma classe de MainWindow que é compartilhada entre os outros aplicativos e o arquivo DataDisplay1Control.xaml mostrado na Figura 2 . MainWindow acessa o controle para exibir o gráfico de dispersão, mas também inclui um TextBox para inserir uma contagem de item, um botão para começar a criar a coleta e objetos de TextBlock que exibem o tempo decorrido durante três estágios dos processos que culminate na exibição do gráfico.

Por padrão, o número de itens é 1.000, e o programa parece funcionar bem. Mas definir o número de itens para 10.000 e há um atraso antes da exibição mostrada na Figura 3 , que também ilustra muito o artificiality dos dados gerados.

fig03.gif

A Figura 3 O vídeo DataDisplay1

Três tempos decorridos são exibidos. Quando você clica no botão, o manipulador Click em MainWindow.xaml.cs começa criando um objeto do tipo DataCollection com o número especificado de pontos de dados. A primeira vez que decorrido é para a criação desta coleção. A coleção, em seguida, é definida como o DataContext da janela. Que é a segunda hora de decorrido. O tempo decorrido terceiro é o tempo necessário para o ItemsControl exibir os pontos de dados resultante. Para calcular esse terceiro tempo decorrido, eu usei o evento LayoutUpdated com falta de uma alternativa melhor.

Como você pode ver, a maior parte das vezes envolve a atualização a exibição; na minha máquina a média dos três tentativas foi 7.7 segundos. Isso é bastante atrapalhar, especialmente considerando que este programa realmente tem nada de errado com ele. Ele está usando recursos do WPF no modo como eles foram objetivo. O que exatamente está acontecendo aqui?

Quando o programa define a propriedade de DataContext como um objeto do tipo DataPointCollection, o ItemsControl recebe uma notificação de alteração de propriedade para a propriedade ItemsSource. ItemsControl enumera através da coleção de objetos DataPoint, e para cada objeto DataPoint cria um objeto do ContentPresenter. (A classe do ContentPresenter deriva do FrameworkElement; este é o mesmo elemento que derivados do ContentControl como botão e o Windows usa para exibir conteúdo do controle.)

Para cada objeto do ContentPresenter, a propriedade Content é definida como o objeto DataPoint correspondente e a propriedade do ContentTemplate é definida como a propriedade ItemTemplate ItemsControl. Esses objetos ContentPresenter, em seguida, são adicionados ao painel usado por ItemsControl para exibir seus itens — nesse caso, uma grade de célula única.

Esta parte do processo vai rapidamente. A parte demorada é quando o ItemsControl deve ser exibido. Porque o painel tem acumulados filhos novos, o método de MeasureOverride é chamado e é esta chamada que requer 7.7 segundos para executar para itens 10.000.

MeasureOverride método o painel chama o método medida de cada um dos seus filhos do ContentPresenter. Se o filho do ContentPresenter não criou ainda uma árvore visual para exibir seu conteúdo, ele agora faça isso. O ContentPresenter cria essa árvore visual com base no modelo armazenado na sua propriedade ContentTemplate. O ContentPresenter também deve configurar as ligações de dados entre as propriedades dos elementos dessa árvore visual e as propriedades do objeto armazenado na sua propriedade Content (no exemplo, o objeto DataPoint). O ContentPresenter, em seguida, chama o método de medida do elemento raiz desta árvore visual.

Se você estiver interessado em Explorando esse processo em mais detalhes, a DLL DataLibrary inclui uma classe SingleCellGrid nomeado que permite que você investigar dentro do painel. No arquivo DataDisplay1Control.xaml, você pode substituir apenas grade com dados: SingleCellGrid.

Quando uma caixa de listagem contém muitos itens, mas exibe somente alguns, ele ignora muita esse trabalho inicial porque por padrão, ele usa um VirtualizingStackPanel que cria filhos somente como eles estão sendo exibidos. Isso não é possível com um gráfico de dispersão, no entanto.

Loops ocultos

O loop é a construção fundamental da programação de computador. A única razão que usam computadores é gravar loops que executam tarefas repetitivas. Loops parecem ser desaparecendo da nossa experiência em programação. Em linguagens de programação funcionais como F #, loops são relegated para programação de estilo antigo e muitas vezes são substituídos por operações que funcionam em todo matrizes, listas e conjuntos. Da mesma forma, operadores de consulta em LINQ realizar operações sobre coleções sem loop explícita.

Essa movimentação fora do loop explícita não é apenas uma alteração em programação estilo mas um desenvolvimento evolutiva essencial para manter o ritmo com as alterações de hardware do computador. Novos computadores hoje rotineiramente têm dois ou quatro processadores; nos anos à frente, verá máquinas com centenas de processadores que executam em paralelo. Loops que são tratados em segundo plano em idiomas ou estruturas de desenvolvimento de programação mais facilmente podem aproveitar de paralelo processamento sem qualquer trabalho especiais pelo programador.

Até que futuro mágica, no entanto, deve permanecer ciente de que os loops ainda existem embora nós não é possível vê-los e como um programador prudente observado uma vez, "não ain't tal coisa como um loop livre."

O DataTemplate definido dentro de seção de um ItemsControl ItemTemplate está dentro de um loop oculto. Que DataTemplate é invocada para a criação de milhares de elementos e outros objetos, bem como o estabelecimento de ligações de dados.

Se, na verdade, tivéssemos código o loop, provavelmente todos será muito mais cuidadosos criação que DataTemplate. Devido a seu impacto no desempenho, ajustar o DataTemplate é vale a pena o tempo e esforço. Praticamente tudo o que fazer para ela provavelmente terá um impacto perceptível no desempenho. Geralmente, apenas quanto impacto (e em qual direção) pode ser difícil prever, portanto, você provavelmente vai querer experimentar várias abordagens.

Em geral, você vai querer simplificar a árvore visual no DataTemplate. Tente minimizar o número de elementos, objetos e ligações de dados.

Vá para DataDisplay1Control.xaml e tente eliminando as ligações de dados nos itens TextBlock a dica de ferramenta. (Simplesmente você pode inserir qualquer caractere na frente das chaves à esquerda.) Você deve shave algumas décimos de segundo o tempo anterior de segundos 7.7 dedicado.

Agora comentário toda a seção dica de ferramenta, e você poderá ver a hora exibição solte para baixo para 4,7 segundos. Definir uma propriedade de preenchimento no elemento Path para algumas cores e comente a seção inteira do estilo e agora o tempo de exibição cai para baixo para segundos 3.5. Remova a transformação do elemento Path e virão até cerca de 1 segundo.

Obviamente, agora é inútil porque ele não está exibindo os dados, mas você provavelmente pode ver como você pode começar a ter uma noção para o impacto desses itens. É apenas um pouco de marcação, mas vale a pena muita experimentação.

Aqui é uma alteração que melhora o desempenho e legibilidade sem hurting funcionalidade: substituir o conteúdo das marcas Path.ToolTip com o seguinte:

<TextBlock>
    <TextBlock.Text>
        <MultiBinding StringFormat="{}{0}, X={1}, Y={2}">
            <Binding Path="ID" />
            <Binding Path="VariableX}" />
            <Binding Path="VariableY}" />
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

A opção StringFormat ligações é nova no .NET 3.5 SP1, e usá-lo aqui obtém o tempo de exibição abaixo de segundos 7.7 para 6.4.

Usando conversores de valor

Ligações de dados, opcionalmente, podem referenciar classes pouco chamados conversores de valor, que implementam a interface IValueConverter ou do IMultiValueConverter. Converter de métodos nos conversores de valor chamados e ConvertBack Executar conversão de dados entre a origem da ligação e o destino.

Conversores são geralmente generalizados a ser aplicável em uma variedade de aplicações; um exemplo é o BooleanToVisibilityConverter útil usado para converter true e false Visibility.Visible e Visibility.Collapsed, respectivamente. Mas conversores podem ser como ad hoc quantas forem necessárias possam ser.

Para simplificar o DataTemplate e reduzir o número de ligações de dados, dois conversores pôde ser criados. O conversor mostrado na Figura 4 é denominado IndexToBrushConverter (incluído na DLL DataLibrary) e converte um inteiro não negativo em um pincel. O conversor tem uma propriedade pública chamada pincéis do tipo matriz de pincel e o número inteiro é simplesmente um índice para essa matriz.

Figura 4 A classe IndexToBrushConverter

public class IndexToBrushConverter : IValueConverter
{
    public Brush[] Brushes { get; set; }

    public object Convert(object value, Type targetType, 
                          object parameter, CultureInfo culture)
    {
        return Brushes[(int)value];
    }

    public object ConvertBack(object value, Type targetType, 
                              object parameter, CultureInfo culture)
    {
        return null;
    }
}

Em uma seção de recursos de um arquivo XAML, o conversor pode ser instanciado com um grupo de x: matriz assim:

<data:IndexToBrushConverter x:Key="indexToBrush">
    <data:IndexToBrushConverter.Brushes>
        <x:Array Type="Brush">
            <x:Static Member="Brushes.Red" />
            <x:Static Member="Brushes.Yellow" />
            <x:Static Member="Brushes.Green" />
            <x:Static Member="Brushes.Cyan" />
            <x:Static Member="Brushes.Blue" />
            <x:Static Member="Brushes.Magenta" />
        </x:Array>
    </data:IndexToBrushConverter.Brushes>
</data:IndexToBrushConverter>

Você, em seguida, pode substituir a toda seção de estilo mostrada na A Figura 2 com uma ligação que faz referência este conversor:

<Path Fill="{Binding Type, Converter={StaticResource indexToBrush}}">

O conversor segundo em DataLibrary é chamado DoublesToPointConverter. Este conversor implementa a interface de IMultiValueConverter para criar um ponto de duas dobras, X e Y. Observe que usando esse conversor permite que a propriedade de centro do Ellipse­Geometry a ser definida diretamente, eliminando a necessidade de TranslateTransform.

O projeto DataDisplay2 inclui esses conversores e usa a abordagem StringFormat a dica de ferramenta, mas os horários são disappointing: segundos 7.7 com a dica de ferramenta, 4.4 segundos sem e 3.4 com uma constante preenchimento Pincel.

Normalmente, seria espero os conversores para melhorar o desempenho. Eu não tenho certeza de que por que não é o caso aqui, mas não se surpreenda se fosse um problema de boxing e unboxing. Em DoublesToPointConverter, por exemplo, os valores de entrada duplos precise ser em caixa e unboxed, e o ponto de saída tem de ser em caixa e unboxed.

Procurar sem Freezables!

Se desejar ver o desempenho do DataDisplay2 realmente degradar, tente substituir esses membros x: estática no Pincel:

<x:Static Member="Brushes.Red" />

com objetos SolidColorBrush:

<SolidColorBrush Color="Red" />

O tempo de exibição leaps up para 20 segundos e além! Ainda o x: estáticos e elementos SolidColorBrush parecem praticamente o mesmo. A propriedade de Brushes.Red estática retorna um SolidColorBrush com cor definida como vermelho.

Mas lembre-se que SolidColorBrush deriva de Freezable. A propriedade Brushes.Red retorna um SolidColorBrush que está congelada e segmento seguro. O valor não pode ser alterado. Quando esse Pincel é passado para o sistema composição visual, podem ser tratada como uma constante.

SolidColorBrush explícita, no entanto, não está congelada. Ele permanece ativo no sistema de composição visual e responderá a alterações na sua propriedade de cor. Que potentiality dinâmico é mais complexa para o sistema manipular e que revela próprio em degradação de desempenho.

Como resultado, observe que você deve congelar qualquer objeto congelável que não será alterado. No código, você chamar o método de congelamento; no XAML, você pode usar a opção PresentationOptions:Freeze. Com freqüência você verá que a diferença será insignificante, mas com o uso de pincéis descongeladas em objetos gráficos 10.000, obviamente, é um fator crucial.

Um apresentador intermediário

Em muitos arquiteturas de aplicativo comuns, é comum ter algum tipo de intermediário entre a interface de usuário e os objetos comerciais reais (neste exemplo, DataPoint e DataCollection). Check-out de relação de tradição, chamarei este intermediário "apresentador" (embora apresentadores tradicionais proceda em vez disso, mais o que poderá ser exibida aqui).

Uma das funções deste apresentador pode ser tornar as informações do objeto comercial mais amenable para ligações para a interface do usuário. Por exemplo, o objeto de negócios DataPoint tem propriedades denominadas VariableX e VariableY do tipo duplo. O DataPointPresenter classe (como eu irá chamá-lo) pode, em vez disso, tenha uma propriedade chamado variável do tipo Point.

Para fins de desempenho que escrevi DataPointPresenter para derivar a partir DataPoint. Além da propriedade variável, ele também define propriedades denominadas Pincel (do tipo Brush) e a dica de ferramenta (do tipo seqüência de caracteres). a Figura 5 mostra uma versão abreviada levemente da classe DataPointPresenter.

Figura 5 A classe DataPointPresenter

public class DataPointPresenter : DataPoint
{
    static readonly Brush[] _brushes = 
    { 
        Brushes.Red, Brushes.Yellow, Brushes.Green, 
        Brushes.Cyan, Brushes.Blue, Brushes.Magenta 
    };
    Point _variable;
    Brush _brush;
    string _tooltip;

    public Point Variable [...]
    public Brush Brush [...]
    public string ToolTip [...]

    protected override void OnPropertyChanged(string propertyName)
    {
        switch (propertyName)
        {
            case "VariableX":
            case "VariableY":
                Variable = new Point(VariableX, VariableY);
                goto case "ID";

            case "ID":
                ToolTip = String.Format("{0}, X={1}, Y={2}",
                                        ID, VariableX, VariableY);
                break;

            case "Type":
                Brush = _brushes[Type];
                break;
        }
        base.OnPropertyChanged(propertyName);
    }
}

A DLL DataLibrary também contém uma classe DataCollectionPresenter que é idêntica à DataCollection, exceto que ele mantém uma coleção de objetos DataPointPresenter.

O projeto DataDisplay3 incorpora essas classes do apresentador. O DataTemplate todo tem esta aparência:

<DataTemplate DataType="data:DataPointPresenter">
    <Path Fill="{Binding Brush}"
          ToolTip="{Binding ToolTip}">
        <Path.Data>
            <EllipseGeometry Center="{Binding Variable}"
                             RadiusX="0.003" RadiusY="0.003" />
        </Path.Data>
    </Path>
</DataTemplate>

Essa abordagem funciona muito melhor do que os conversores, colocar o tempo de exibição para baixo para segundos 3,3 com todos os recursos, incluindo a dica de ferramenta. A desvantagem grande aqui — e você pode shrug ele logoff ou considere a ele uma deficiência grave — que os pincéis vários agora estão codificados na classe DataPointPresenter. Isso não é ideal. No WPF programação ele sempre é bom poder fechar os arquivos de código enquanto continua a ajustar o XAML.

Ter os pincéis no arquivo XAML é preferencial, especialmente se os valores de tipo já aumentar além o número 5. Uma abordagem pode ser armazenar a matriz de pincéis na seção recursos do arquivo XAML do aplicativo e fazer a primeira instância do DataPointPresenter acessá-los e armazená-los em uma variável estática. Agora que você já viu quanto impacto o apresentador neste exemplo, eu não ser usando-lo nos métodos restantes.

Elemento de dados personalizado

Para desenhar os pontos pequenos no gráfico de dispersão, eu estava usando um elemento de caminho com sua propriedade de data definida como um EllipseGeometry. Como EllipseGeometry tem uma propriedade de centro do tipo Point, tive que criar um conversor ou um apresentador para obter um objeto de ponto de duas propriedades do tipo duplo.

Outra solução é substituir essa combinação de caminho e EllipseGeometry com um derivativo do FrameworkElement É personalizado que desenha um ponto. Desde que esteja escrevendo-sozinhos, ele pode ter CenterX e CenterY propriedades do tipo duplo, em vez de uma propriedade Center do tipo Point. Em vez de uma propriedade de preenchimento do tipo Brush, ele pode ter uma propriedade FillIndex do tipo int indexa uma propriedade pincéis do tipo matriz de pincel.

Uma classe (chamada DataDot no projeto DataLibrary) é bastante trivial, consiste principalmente em propriedades de dependência e propriedades CLR que quebrar as propriedades de dependência. MeasureOverride consiste em uma linha:

return new Size(CenterX + RadiusX, CenterY + RadiusY);

OnRender é quase tão simples:

if (Brushes != null)
    dc.DrawEllipse(Brushes[FillIndex], null, 
                   new Point(CenterX, CenterY), 
                   RadiusX, RadiusY);

No projeto DataDisplay4, DataDot aparece no DataTemplate como este:

<data:DataDot CenterX="{Binding VariableX}" 
              CenterY="{Binding VariableY}"
              Brushes="{StaticResource brushes}"
              FillIndex="{Binding Type}"
              RadiusX="0.003" RadiusY="0.003">

A chave de recurso pincéis faz referência a um elemento de x: matriz que contém seis cores.

Exibe o projeto DataDisplay4 que usa esse elemento personalizado próprio em segundos 3,3 com a dica de ferramenta e em 2,5 segundos sem, que é uma melhoria de desempenho excelente considerando a natureza simples da classe DataDot.

Biting the mais

Se você tiver removido o DataTemplate para baixo para o número tiniest de elementos e objetos e reduziu as ligações de dados para o mínimo possível, e você fez tudo você pode pensar e desempenho ainda não é bom o suficiente para você, talvez é hora de surpreender the mais.

E por que QUERO dizer, talvez, é hora de confirmar que a combinação de um ItemsControl e um DataTemplate é realmente poderosa, mas talvez a não bastante a solução é necessário para este aplicativo específico. Esta confirmação não constitui um abandonment do WPF: você ainda vai usar WPF para implementar sua solução. Ele é simplesmente um reconhecimento que a criação de segundo plano de 10.000 derivados do FrameworkElement talvez não é o uso mais eficiente de recursos. A alternativa é um derivativo do FrameworkElement É personalizado que faz a coisa toda — um elemento em vez de 10.000 elementos.

A classe ScatterPlotRender na DLL DataLibrary deriva do FrameworkElement e tem três propriedades de dependência: ItemsSource do tipo ObservableNotifiableCollection <datapoint>, pincéis do tipo matriz de pincel e plano de fundo do tipo Brush.

Você pode lembrar a classe ObservableNotifiableCollection da minha coluna" Propriedades de dependência e notificações"a de 2008 setembro emitir de MSDN Magazine. Essa classe requer que seus membros implementem a interface INotifyPropertyChanged. A classe aciona um evento não apenas quando objetos são adicionados ou removidos da coleção, mas quando alterar propriedades de objetos na coleção. Isso é como ScatterPlotRender será alertado quando as propriedades VariableX e VariableY dos objetos DataPoint são alteradas.

A classe ScatterPlotRender lida com esses eventos de uma maneira muito simples. Sempre que as alterações da propriedade ItemsSource, ou as alterações da coleção; ou uma propriedade dos objetos da coleção DataPoint é alterado, ScatterPlotRender chama InvalidateVisual. Isso gera uma chamada para OnRender, que desenha o gráfico de dispersão inteiro. O código é mostrado na Figura 6 .

A Figura 6 OnRender

protected override void OnRender(DrawingContext dc)
{
    dc.DrawRectangle(Background, null, new Rect(RenderSize));

    if (ItemsSource == null || Brushes == null)
        return;

    foreach (DataPoint dataPoint in ItemsSource)
    {
        dc.DrawEllipse(Brushes[dataPoint.Type], null, 
            new Point(RenderSize.Width * dataPoint.VariableX,
                      RenderSize.Height * dataPoint.VariableY), 1, 1);
    }
}

Observe que os valores VariableX e VariableY são multiplicados pela largura e altura do elemento, que provavelmente poderia ser definido no arquivo XAML. O projeto DataDisplay5 tem um arquivo XAML que instancia um objeto ScatterPlotRender, assim:

<data:ScatterPlotRender Width="300" Height="300"
                        Background="Azure" 
                        ItemsSource="{Binding DataPoints}"
                        Brushes="{StaticResource brushes}" />

A chave de recurso pincéis faz referência a uma matriz de congelado SolidColor­Brush objetos.

A boa notícia é que este gráfico de dispersão aparece na tela muito rapidamente. Desenho em seu OnRender o método é a maneira mais rápida que um elemento do WPF pode ser apresentado visualmente. A má notícia é que o elemento continua a totalmente redesenhado sempre que as alterações de propriedade VariableX ou VariableY e isso acontece cada décimo de segundo. Versões anteriores usado aproximadamente 10 % do tempo da CPU para atualizar próprios (em minha máquina). Esse é up na região de 30 %. Se seu aplicativo exibe dados atualizados com freqüência, convém para refinar a maneira de você executar o desenho (provenientes seguir).

A desvantagem grande é que agora não há nenhuma dica de ferramenta. Uma dica de ferramenta não é impossível nesta classe, mas é bastante confuso. Farei uma dica de ferramenta na próxima versão.

A solução DrawingVisual

Desenha uma classe que deriva FrameworkElement É geralmente propriamente dito no método OnRender, mas ele também pode ter uma aparência visual, mantendo uma coleção de filhos visual. Esses filhos são exibidos na parte superior de qualquer item que o método OnRender desenha.

Filhos Visual podem ser qualquer coisa que deriva de Visual, que inclui o FrameworkElement e controle, mas um derivativo do FrameworkElement É também pode criar filhos visual comparativamente simples na forma de DrawingVisual objetos.

Se um derivativo do FrameworkElement É criar objetos de DrawingVisual, ele normalmente armazena-los em uma coleção VisualChildren, que lida com algumas da sobrecarga envolvida na manutenção visual filhos. A classe ainda precisa substituir VisualChildrenCount e GetVisualChild, no entanto.

A classe ScatterPlotVisual funciona através da criação de um objeto DrawingVisual para cada DataPoint. Quando as propriedades de um objeto DataPoint alterar, a classe só precisa alterar o DrawingVisual associado que DataPoint.

Como a classe ScatterPlotRender, a classe de ScatterPlotVisual define propriedades de dependência denominadas ItemsSource, pincéis e plano de fundo. Ele também mantém uma coleção de VisualChildren que deve ser mantida sincronizada com a coleção ItemsSource. Se um item for adicionado à coleção ItemsSource, um novo visual deve ser adicionado à coleção VisualChildren. Se um item for removido da coleção ItemsSource, o visual correspondente deve ser removido da coleção VisualChildren. Se a propriedade VariableX ou VariableY de um item na coleção ItemsSource muda, o item correspondente na coleção VisualChildren deverá alterar.

Para ajudar a sincronização, a coleção VisualChildren não armazena objetos do tipo DrawingVisual, na verdade, mas em vez disso, mantém objetos do tipo DrawingVisualPlus, que é definido interno para a classe ScatterPlotVisual assim:

class DrawingVisualPlus : DrawingVisual
{
    public DataPoint DataPoint { get; set; }
}

Essa propriedade adicional torna fácil localizar um objeto de DrawingVisualPlus específico na coleção VisualChildren correspondente a um objeto DataPoint específico.

O projeto DataDisplay6 que implementa essa abordagem é a melhor abordagem geral. Tenho certeza de que o tempo de criação de inicialização é um pouco maior que DataDisplay5, mas é quase não perceptível, e a sobrecarga de atualização é consideravelmente reduzida.

Se você pular até 100.000 pontos de dados, no entanto, WPF novamente buckles sob o esforço. Nesse nível de saída de elementos gráficos, estou medo de que você fique sem sugestões. (Talvez obter um computador mais rápido?)

Envie suas dúvidas e comentários para mmnet30@Microsoft.com.

Charles Petzold é editor colaborador longtime para MSDN Magazine. Seu livro mais recente é The Annotated Turing: A Guided Tour through Alan Turing's Historic Paper on Computability and the Turing Machine (Wiley, 2008). Seu site é www.charlespetzold.com.