Introdução

O Office 2007 trouxe mudanças significativas para esta suíte de aplicativos: a mais visível foi o Ribbon, uma nova interface de usuário que facilita muito o uso dos aplicativos.

Outra mudança, menos visível à primeira vista, permite que os aplicativos do Office se integrem com uma grande variedade de programas: o novo formato de arquivo. Até as versões anteriores, o formato de arquivo era proprietário: quando se queria abrir ou gravar um documento do Office em nossas aplicações, era necessário usar automação OLE, o que requeria que o Office estivesse instalado na máquina do cliente, ou então tentar descobrir o formato dos arquivos, que não era completamente documentado e podia ser alterado a cada nova versão.

O novo formato de arquivo, além de documentado, é baseado em padrões abertos, o que permite que qualquer aplicação, em qualquer linguagem ou plataforma possa abrir ou criar arquivos Office 2007. Este novo padrão, chamado de Open XML, é baseado em compactação zip e arquivos XML,  gera arquivos com menor tamanho que os anteriores e permite que outras aplicações abram e alterem estes arquivos.

As possibilidades que se abrem são inúmeras:

  • Programas que permitem indexar e pesquisar textos a partir dos arquivos no disco
  • Programas para geração de documentos em lotes, a partir de bancos de dados e modelos
  • Programas para substituição de textos em lotes
  • Processadores de texto simplificados que geram arquivos Office
  • Geradores de planilhas eletrônicas a partir de dados provenientes de diversas fontes

Neste artigo, iremos mostrar o novo formato de arquivos e como podemos acessá-los e criá-los usando o Delphi, sem a necessidade de instalar o Office.

Analisando um arquivo OpenXML

Um arquivo OpenXML, seja ele um documento (docx), planilha (xlsx) ou apresentação (pptx) é, na realidade, um arquivo zip composto de diversas pastas e arquivos XML. Podemos ver isso na prática, criando um documento no Word composto de algum texto e uma imagem. Ao salvar este documento, podemos renomear o arquivo docx para zip e abri-lo com qualquer programa que abra arquivos zip:

Como podemos ver na Figura acima, o arquivo contém, na raiz, três diretórios, _rels, docProps e word e um arquivo, [Content_Types].xml. Esta estrutura de diretórios é criada pelo Word, e não é obrigatório que ela seja mantida. A localização dos arquivos fica no arquivo .rels, que está no diretório _rels. Este arquivo contém as relações entre o pacote e os arquivos de nível superior.  O seguinte código mostra o arquivo .rels criado em nosso exemplo:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail" Target="docProps/thumbnail.wmf"/>
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
</Relationships>

Analisando este arquivo, vemos o seguinte:

  • As propriedades principais (core-properties) estão no arquivo docProps/core.xml.
  • A imagem reduzida (thumbnail) está em docProps/thumbnail.wmf.
  • O documento principal (officeDocument) está em word/document.xml.
  • As propriedades estendidas (extended-properties) estão em docProps/app.xml.

Com base nestas informações, podemos abrir os arquivos que compõem o pacote OpenXML.

Adicionalmente, podemos ver que no diretório word existem um subdiretório _rels, que contém as relações para o documento. No arquivo document.txt.rels, encontramos as seguintes relações:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
  <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
  <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>
</Relationships>

Aqui são encontradas as relações para o documento. Vemos que os estilos aplicados no documento encontram-se no arquivo styles.xml e que a imagem que adicionamos está em media/image1.png. Assim, podemos acessar qualquer parte do documento.

A seguir iremos criar um pequeno programa em Delphi que abre um arquivo do Office e lista suas propriedades (tanto as principais como as estendidas) em um componente TValueListEditor.

Acessando os arquivos OpenXML

Para acessar os arquivos OpenXML , precisamos dividir nosso programa nas seguintes partes:

  • Abrir o pacote OpenXML com um componente que permite ler e gravar arquivos zip
  • Abrir o arquivo .rels em _rels e ler as relações, extraindo a localização das partes que nos interessam
  • Acessar as partes, executando a funcionalidade desejada.

Para abrir os arquivos Zip, usaremos o componente TZipFile, que está disponível no Delphi a partir da versão XE2. Este componente permite manipular arquivos zip de maneira relativamente simples. Crie um novo projeto no Delphi e adicione à Form um botão, um OpenDialog e um Memo.

Configure a propriedade Caption para Abrir. Configure a propriedade Filter do OpenDialog para “Arquivos Word (*.docx, *.docm)|*.docx;*.docm| Arquivos Excel (*.xlsx, *.xlsm)|*.xlsx;*.xlsm| Arquivos Powerpoint (*.pptx, *.pptm)|*.pptx;*.pptm”.

No handler do evento OnClick do botão, coloque o seguinte código:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  ZipStream: TStream;
  XmlNode: IXMLNode;
  i: Integer;
  AttType: String;
  ZipFile: TZipFile;
  LocalHeader: TZipHeader;
begin
  if OpenDialog1.Execute then begin
    ZipFile := TZipFile.Create();
    try
      ZipFile.Open(OpenDialog1.FileName, TZipMode.zmRead);
      try
        // Lê relações
        ZipFile.Read('_rels/.rels', ZipStream, LocalHeader);
        ZipStream.Position := 0;
        XMLDocument1.LoadFromStream(ZipStream);
        Memo1.Text := XMLDoc.FormatXMLData(XMLDocument1.XML.Text);
      finally
        ZipStream.Free;
      end;
    finally
      ZipFile.Close();
      ZipFile.Free;
    end;
  end;
end;

Se o usuário escolher um arquivo, indicamos o nome do arquivo para o componente ZipFile e depois extraímos o arquivo .rels para um stream e carregamos as linhas do Memo com este stream, formatado com a função FormatXMLData. A figura a seguir mostra o resultado da execução.

Uma vez que temos o arquivo .rels, devemos lê-lo e interpretar as relações. Poderíamos usar as funções de leitura de arquivos texto e interpretar o documento, porém essa não é a melhor maneira de fazer esta operação. O ideal é usar um componente próprio para a leitura de arquivos XML, como o componente TXMLDocument, que vem com o Delphi na guia Internet da paleta de componentes.

Coloque dois componentes TXMLDocument e um componente TValueListEditor na Form.  Modifique a propriedade TitleCaptions do TValueListEditor para Propriedade/Valor. No evento OnClick do botão, modifique o código para:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  ZipStream: TStream;
  XmlNode: IXMLNode;
  i: Integer;
  AttType: String;
  ZipFile: TZipFile;
  LocalHeader: TZipHeader;
begin
  if OpenDialog1.Execute then begin
    ZipFile := TZipFile.Create();
    try
      ZipFile.Open(OpenDialog1.FileName, TZipMode.zmRead);
      // Lê relações
      ZipFile.Read('_rels/.rels', ZipStream, LocalHeader);
      try
        ZipStream.Position := 0;
        XMLDocument1.LoadFromStream(ZipStream);
        Memo1.Text := XMLDoc.FormatXMLData(XMLDocument1.XML.Text);
        ValueListEditor1.Strings.Clear;
        // Processa nós
        for i := 0 to XMLDocument1.DocumentElement.ChildNodes.Count - 1 do begin
          XmlNode := XMLDocument1.DocumentElement.ChildNodes.Nodes[i];
          // Pega o tipo de relação.
          // Ela é a parte final do atributo Type
          AttType := ExtractFileName(XmlNode.Attributes['Type']);
          if AttType.EndsWith('core-properties') or
             AttType.EndsWith('extended-properties') then
            // Adiciona as propriedades no ValueListEditor
            LePropriedades(ZipFile, XmlNode.Attributes['Target']);
        end;
      finally
        ZipStream.Free;
      end;
    finally
      ZipFile.Close();
      ZipFile.Free;
    end;
  end;
end;

Carregamos o stream em XMLDocument1 e processamos os nós, para encontrar aqueles com os tipos que queremos (core-properties ou extended-properties). Quando os encontramos, passamos o nome do arquivo (que está no atributo Target) para a função LePropriedades, que irá ler o arquivo de propriedades e adicioná-las ao ValueListEditor. A função LePropriedades é:

procedure TMainFrm.LePropriedades(ZipFile: TZipFile; const Arquivo: String);
var
  ZipStream: TStream;
  i: Integer;
  XmlNode: IXMLNode;
  LocalHeader: TZipHeader;
begin
  ZipFile.Read(Arquivo, ZipStream, LocalHeader);
  try
    ZipStream.Position := 0;
    XMLDocument2.LoadFromStream(ZipStream);
    // Lê as propriedades
    for i := 0 to XMLDocument2.DocumentElement.ChildNodes.Count - 1 do
    begin
      XmlNode := XMLDocument2.DocumentElement.ChildNodes.Nodes[i];
      try
        // Achou nova propriedade adiciona
        ValueListEditor1.InsertRow(XmlNode.NodeName, XmlNode.NodeValue, True);
      except
        // Propriedade não é um valor simples - despreza.
        On EXMLDocError do;
        // Propriedade é nula - adiciona sting nulo
        On EVariantTypeCastError do
          ValueListEditor1.InsertRow(XmlNode.NodeName, '', True);
      end;
    end;
  finally
    ZipStream.Free;
  end;
end;

Esta função é parecida com a anterior. Iremos ler o arquivo de propriedades no segundo TXMLDocument e inserir uma linha no ValueListEditor para cada propriedade encontrada. Tratamos aqui dois tipos de exceção: EXMLDocError, que pode acontecer quando o tipo de dado não é um tipo simples, como um string ou inteiro e EVariantTypeCastError, que acontece quando o valor é nulo. Desta maneira, adicionamos as propriedades na lista, como mostra a figura a seguir.

Como podemos ver, o acesso aos dados de um arquivo OpenXML é relativamente simples, e pode ser feito usando componentes disponíveis no Delphi, mas isso não é tudo que pode ser feito: como estamos trabalhando com pacotes zip normais e arquivos XML, usando tecnologia aberta, podemos também modificar os arquivos, usando as mesmas técnicas.

Até agora, vimos como acessar um arquivo. Porém, nada impede que possamos criar um arquivo a partir de nossos dados.

Criação de um arquivo OpenXML

Para gerar um arquivo OpenXML, precisamos criar alguns arquivos que irão compor o pacote. O pacote mínimo deve conter três arquivos:

  • [Content_Types].xml
  • _rels/.rels
  • xml

Não é necessário criar uma estrutura de diretórios como a do Word, basta apenas que indiquemos no arquivo .rels a localização dos dados. À medida que  vamos adicionando novas funcionalidades, como imagens, cabeçalhos, temas e estilos, precisamos incluir novos arquivos para adicionar estas partes ao documento. Inicialmente, iremos criar um arquivo simples, para mostrar o processo de geração de um arquivo e, em seguida, mostraremos como criar um arquivo mais complexo.

Crie um novo projeto e coloque um Label, um Memo e um botão na Form. Mude a propriedade Caption do Label para Texto:, a propriedade Caption do Button para Criar e limpe a propriedade Lines do Memo.

Coloque um componente XMLDocument. No evento OnClick do botão, coloque o seguinte código:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  zipFile: TZipFile;
  contentTypes: TStream;
  rels: TStream;
  doc: TStream;
begin
  zipFile := TZipFile.Create();
  try
    zipFile.Open('ArquivoSimples.docx', TZipMode.zmWrite);
    contentTypes := CriaContentTypes();
    try
      zipFile.Add(contentTypes, '[Content_Types].xml');
    finally
      contentTypes.Free;
    end;
    rels := CriaRels();
    try
      zipFile.Add(rels, '_rels\.rels');
    finally
      rels.Free;
    end;
    doc := CriaDocumento();
    try
      zipFile.Add(doc, 'word\document.xml');
    finally
      doc.Free;
    end;
  finally
    zipFile.Close();
    zipFile.Free;
  end;
end;

O programa irá criar os diversos arquivos necessários, adicionar os streams para o arquivo zip e criar um arquivo com o nome ArquivoSimples.docx. A função que cria o arquivo [Content_Types.xml] é:

function TMainFrm.CriaContentTypes(): TStream;
var
  Root: IXmlNode;
  Tipo: IXmlNode;
  XMLDoc: IXmlDocument;
begin
  Result := TMemoryStream.Create();
  XMLDoc := CriaXml;
  // Nó raiz
  Root := XMLDoc.addChild('Types',
    'http://schemas.openxmlformats.org/package/2006/content-types');
  // Definição de tipos
  Tipo := Root.addChild('Default');
  Tipo.Attributes['Extension'] := 'rels';
  Tipo.Attributes['ContentType'] :=
    'application/vnd.openxmlformats-package.relationships+xml';
  Tipo := Root.addChild('Default');
  Tipo.Attributes['Extension'] := 'xml';
  Tipo.Attributes['ContentType'] :=
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml';
  // Grava no stream de saída
  XMLDoc.SaveToStream(Result);
  Result.Position := 0;
end;

A função que cria o arquivo de relações é:

function TMainFrm.CriaRels(): TStream;
var
  Root: IXmlNode;
  Rel: IXmlNode;
  XMLDoc: IXmlDocument;
begin
  Result := TMemoryStream.Create();
  XMLDoc := CriaXml;
  // Nó raiz
  Root := XMLDoc.addChild('Relationships',
    'http://schemas.openxmlformats.org/package/2006/relationships');
  // Definição de relações
  Rel := Root.addChild('Relationship');
  Rel.Attributes['Id'] := 'rId1';
  Rel.Attributes['Type'] :=
    'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument';
  Rel.Attributes['Target'] := 'word/document.xml';
  // Grava no stream de saída
  XMLDoc.SaveToStream(Result);
  Result.Position := 0;
end;

O código para gravar o documento com o texto digitado no Memo é:

function TMainFrm.CriaDocumento(): TStream;
var
  Root: IXmlNode;
  XMLDoc: IXmlDocument;
begin
  Result := TMemoryStream.Create();
  XMLDoc := CriaXml;
  // Nó raiz
  Root := XMLDoc.addChild('wordDocument',
    'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
  // Grava texto
  Root.addChild('body').addChild('p').addChild('r').addChild('t').NodeValue :=
    Memo1.Text;
  // Grava no stream de saída
  XMLDoc.SaveToStream(Result);
  Result.Position := 0;
end;

Aqui apenas precisamos gravar um nó dentro do nó raiz wordDocument: ele  é o corpo do documento, que tem um parágrafo (nó p), um “run” (nó r) e o texto, que é o conteúdo do Memo. Ao compilar e executar o programa, podemos digitar um texto no Memo e clicar no botão Criar. O arquivo docx é criado com o texto digitado.

Colocando mais informações no arquivo

Uma vez que sabemos como criar nossos arquivos, podemos adicionar mais informações no que está sendo criado. Criaremos agora um exemplo que mostra as fontes disponíveis no sistema. Este documento será gerado em formato paisagem, e colocaremos um cabeçalho com três colunas e o número da página.

Crie um novo projeto e coloque um botão, um XmlDocument  Altere a propriedade Caption do botão para Criar. No evento OnClick do botão, coloque:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  ZipFile: TZipFile;
  MemStream: TMemoryStream;
begin
  ZipFile := TZipFile.Create();
  try
    ZipFile.Open('ArquivoComplexo.docx', TZipMode.zmWrite);
    MemStream := TMemoryStream.Create();
    try
      CriaContentTypes(MemStream);
      ZipFile.Add(MemStream, '[Content_Types].xml');
      MemStream.Clear;
      CriaRels(MemStream);
      ZipFile.Add(MemStream, '_rels\.rels');
      MemStream.Clear;
      CriaDocumento(MemStream);
      ZipFile.Add(MemStream, 'word\document.xml');
    finally
      MemStream.Free;
    end;
  finally
    ZipFile.Close();
    ZipFile.Free;
  end;
end;

As funções CriaRels e CriaContentTypes são as mesmas da rotina anterior. A função CriaDocumento é a seguinte:

procedure TMainFrm.CriaDocumento(AStream: TStream);
var
  Root, Body, PgSz: IXMLNode;
  i: Integer;
  SectPr: IXMLNode;
  Header: IXMLNode;
begin
  LimpaXML;
  CriaCabecalho;
  // Nó raiz
  Root := XMLDocument1.addChild('w:wordDocument');
  Root.DeclareNamespace('w',
    'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
  Body := Root.addChild('w:body');
  for i := 0 to Screen.Fonts.Count - 1 do
    AdicionaFonte(Body, Screen.Fonts[i]);
  
  // Grava no stream de saída
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
end;

Iremos varrer as fontes do sistema, chamando a função AdicionaFonte, que irá adicionar o texto formatado no arquivo Document.xml:

procedure TMainFrm.AdicionaFonte(Body: IXMLNode; NomeFonte: String);
var
  Fonte: IXMLNode;
  Run: IXMLNode;
  RunPr: IXMLNode;
begin
  Run := Body.addChild('w:p').addChild('w:r');
  RunPr := Run.addChild('w:rPr');
  Fonte := RunPr.addChild('w:rFonts');
  Fonte.Attributes['w:ascii'] := NomeFonte;
  Fonte.Attributes['w:hAnsi'] := NomeFonte;
  Fonte.Attributes['w:cs'] := NomeFonte;
  RunPr.addChild('w:sz').Attributes['w:val'] := 30;
  Run.addChild('w:t').NodeValue := NomeFonte;
  Run.addChild('w:tab');
  Run.addChild('w:t').NodeValue :=
    'The quick brown fox jumps over the lazy dog';
end;

Para cada fonte do sistema, adicionamos um parágrafo e, nele, um Run. O Run deve ser formatado com o elemento rPr, colocando-se como filho o elemento rFonts e o nome da fonte como valores dos atributos ascii,  hAnsi e cs. Também mudamos o tamanho da fonte adicionando o elemento sz. Em seguida, colocamos o nome da fonte como texto, adicionando o elemento tab para gerar uma tabulação e um texto de exemplo. Ao rodar o programa, vemos que a lista de fontes é gerada no documento.

O próximo passo é fazer que o documento seja colocado em paisagem. Para isso, devemos adicionar ao final do documento um elemento sectPr (propriedades da seção), que indica a formatação da seção. Coloque o seguinte código ao final de CriaDocumento, antes da linha   XMLDocument1.SaveToStream(AStream):

SectPr := Body.addChild('sectPr');
PgSz := SectPr.addChild('w:pgSz');
PgSz.Attributes['w:w'] := Round(297 / 25.4 * 1440);
PgSz.Attributes['w:h'] := Round(210 / 25.4 * 1440);
PgSz := SectPr.addChild('w:pgMar');
PgSz.Attributes['w:top'] := 1440;
PgSz.Attributes['w:bottom'] := 1440;
PgSz.Attributes['w:left'] := 720;
PgSz.Attributes['w:right'] := 720;
PgSz.Attributes['w:header'] := 720;
PgSz.Attributes['w:footer'] := 720;

Neste código adicionamos o elemento pgSz (Page size), dando os atributos w e h para a largura e altura da página. Estas medidas são em twips (1/1440 de polegada), assim fazemos a conversão do tamanho da página A4 para twips. Em seguida, colocamos o elemento pgMar (Page margins), que determina as margens da página e a posição do cabeçalho e rodapé. Ao rodarmos o programa e abrirmos o documento, vemos que ele está em paisagem.

O último passo é colocar o cabeçalho. Colocamos o cabeçalho em um arquivo separado e, desta maneira, devemos alterar todas as referências para que este novo documento seja lido.

Inicialmente, criamos uma referência para o cabeçalho na seção, como filho de sectPr. Coloque o seguinte código em CriaDocumento, após a linha SectPr := Body.AddChild(‘sectPr’):

Header := SectPr.addChild('w:headerReference');
Header.Attributes['w:type'] := 'default';
Header.Attributes['r:id'] := 'rId1';

Para usar as referências, devemos adicionar um novo namespace ao documento. Isto é feito adicionando a seguinte linha após a declaração do namespace em CriaDocumento:

Root.DeclareNamespace('r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');

Criamos uma referência rId1 no documento. Devemos então criar uma função que cria este relacionamento no arquivo word\_rels\document.xml.rels:

procedure TMainFrm.CriaDocRels(AStream: TStream);
var
  Root: IXMLNode;
  Rel: IXMLNode;
begin
  LimpaXML;
  CriaCabecalho;
  // Nó raiz
  Root := XMLDocument1.addChild('Relationships',
    'http://schemas.openxmlformats.org/package/2006/relationships');
  // Definição de relações
  Rel := Root.addChild('Relationship');
  Rel.Attributes['Id'] := 'rId1';
  Rel.Attributes['Type'] :=
    'http://schemas.openxmlformats.org/officeDocument/2006/relationships/header';
  Rel.Attributes['Target'] := 'header1.xml';
  // Grava no stream de saída
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
end;

Esta função é semelhante à que cria o relacionamento do pacote. A função que cria o cabeçalho no arquivo header1.xml é:

procedure TMainFrm.CriaHeader(AStream: TStream);
var
  Root, Header, PTab: IXMLNode;
begin
  LimpaXML;
  CriaCabecalho;
  // Nó raiz
  Root := XMLDocument1.addChild('w:hdr');
  Root.DeclareNamespace('w',
    'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
  Header := Root.addChild('w:p');
  Header.addChild('w:r').addChild('w:t').NodeValue := 'Texto 1';
  PTab := Header.addChild('w:r').addChild('w:ptab');
  PTab.Attributes['w:relativeTo'] := 'margin';
  PTab.Attributes['w:alignment'] := 'center';
  PTab.Attributes['w:leader'] := 'none';
  Header.addChild('w:r').addChild('w:t').NodeValue := 'Texto 2';
  PTab := Header.addChild('w:r').addChild('w:ptab');
  PTab.Attributes['w:relativeTo'] := 'margin';
  PTab.Attributes['w:alignment'] := 'right';
  PTab.Attributes['w:leader'] := 'none';
  Header.addChild('w:fldSimple').Attributes['w:instr'] := 'PAGE \* MERGEFORMAT';
  // Grava no stream de saída
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
end;

Aqui criamos o cabeçalho com um texto alinhado à esquerda, uma tabulação para alinhar o texto centralizado e outra tabulação para alinhar o número da página à direita. O número da página é dado pelo elemento fldSimple, usando-se  o atributo instr com o valor PAGE \* MERGEFORMAT. Após criar estas funções devemos colocar o código para chamá-las, ao final do evento OnClick do botão:

MemStream.Clear;
CriaDocRels(MemStream);
ZipFile.Add(MemStream, 'word\_rels\document.xml.rels');
MemStream.Clear;
CriaHeader(MemStream);
ZipFile.Add(MemStream, 'word\header1.xml');

Agora, precisamos apenas fazer uma pequena mudança em [Content_Types].xml, adicionando o elemento Override, para determinar o tipo de header1.xml. Coloque o seguinte código em CriaContentTypes, antes da linha XMLDocument1.SaveToStream(AStream):

Tipo := Root.addChild('Override');
Tipo.Attributes['PartName'] := '/word/header1.xml';
Tipo.Attributes['ContentType'] :=
  'application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml';

Com isso, nosso programa está pronto. Ao executá-lo, geramos um documento semelhante ao mostrado na figura a seguir:

Conclusões

O formato OpenXML traz grandes vantagens para quem quer processar e abrir arquivos do Office. Como este formato usa tecnologias abertas e está completamente documentado, podemos acessar, alterar ou mesmo criar arquivos do Office usando quaisquer ferramentas de desenvolvimento (ou mesmo alterando-se os arquivos manualmente), em qualquer plataforma ou linguagem.

Não são necessárias APIs proprietárias ou programas especiais, o que permite que a informação esteja disponível para qualquer um que queira acessá-la. Mostramos aqui como manipular os arquivos em Delphi, fazendo notar que utilizamos apenas componentes padrão do Delphi, utilizando apenas arquivos  zip e XML.

O código fonte para este projeto está em https://github.com/bsonnino/OpenXmlDelphiPort

 

Introduction

One of the interesting things that C#9 brought is the introduction of Code Generators. When compiling the code, the C# compiler can generate extra code and add it to your project, thus complementing your code. This has a lot of possible uses:

  • Add an attribute to your code to generate boilerplate code: you can add an attribute to a private member and the code generator will add the property and the INotifyPropertyChanged to your class.
  • Discover the dependencies needed at compile time and wire them in the executable. This can improve execution time, because wiring the dependencies using reflection is very slow. In this case, the dependencies would be already set up.
  • Parse files and generate code for them: you could have a json file with data and the compiler would add the classes without the need of creating them manually. Once the file has changed (and thus the class structure), a new class would be created.

One interesting use of this feature was used by the Windows SDK Team for using the Win32 APIs in our C# code. Instead of using the traditional PInvoke (which you must go to http://pinvoke.net to get the signatures and structures, add them as external methods and call them), you can use C#/Win32, developed to simplify the usage of Win32 APIs.

Using C#/Win32

To use C#/Win32, you must have .NET 5.102 installed in your machine and you must be using Visual Studio 16.8 or newer. With these prerequisites, you can create a new console application in the command line with:

dotnet new console -o UseWin32

This line will create a new console project in the UseWin32  folder. Then you must add the CsWin32  NuGet package with:

dotnet add package Microsoft.Windows.CsWin32 --prerelease

Now you are ready to open the project in Visual Studio or Visual Studio Code and use it. This first project will  enumerate the files in the current directory. Yes, I know that .NET already has methods to do that, but we can also do it using Win32.

Create a new text file and name it NativeMethods.txt. In this file, add the names of the thre API functions that we need:

FindFirstFile
FindNextFile
FindClose

Now, we are ready to use these methods in our program. In Program.cs, erase everything and add this code:

using System;
using System.Linq;
using Microsoft.Windows.Sdk;

var handle = PInvoke.FindFirstFile("*.*", out var findData);
if (handle.IsInvalid)
    return;

We are using another feature of C#9, Top Level Statements. We can see that CsWin32 has generated a new class named PInvoke  and has added the FindFirstFile  method. If you hoveer the mouse over it, you will see something like this:

As you can see, it has added the function and  also added the documentation for it. If you right-click in the function and select Go to Definition, it will open the generated code:

We can then continue the code to enumerate the files:

using System;
using System.Linq;
using Microsoft.Windows.Sdk;

var handle = PInvoke.FindFirstFile("*.*", out var findData);
if (handle.IsInvalid)
    return;
bool result;
do
{
    Console.WriteLine(ConvertFileNameToString(findData.cFileName.AsSpan()));
    result = PInvoke.FindNextFile(handle, out findData);
} while (result);
PInvoke.FindClose(handle);

string ConvertFileNameToString(Span<ushort> span)
{
    return string.Join("", span.ToArray().TakeWhile(i => i != 0).Select(i => (char)i));
}

This code opens the enumeration with FindFirstFile. If the returned handle is invalid, then there are no files in the folder, so the program exits. Then, it will print the file name to the console and continue the enumeration until the last file, when it calls FindClose, to close the handle. The filename is returned as a structure named __ushort_260, that can be converted to a Span<ushort> with the AsSpan method. To convert this to a string, we use the ConvertFileNameToString method, that uses Linq to convert it to a string: it takes all the items until it finds a 0, converts them to an IEnumerable<char> and then uses string.Join to convert this to a string.

If you use this code, you will see that FindClose(handle) has an error. That’s because the FileClose function receives a parameter of type HANDLE, while the handle variable is of the FileCloseSafeHandle type and both are not compatible (FileCloseSafeHandle has a handle field, but it’s protected and cannot be used). The solution, in this case, is to dispose the handle variable, that will call FindClose. This code shows how this is done:

using var handle = PInvoke.FindFirstFile("*.*", out var findData);
if (handle.IsInvalid)
    return;
bool result;
do
{
    Console.WriteLine(ConvertFileNameToString(findData.cFileName.AsSpan()));
    result = PInvoke.FindNextFile(handle, out findData);
} while (result);

We are using here the C#8’s Using Statement, so we don’t need to use a block. When you run this code, you will see the enumeration of the files in the console:

Function callbacks

As you can see, there is no need to dig to use the Win32 APIs, but there are some APIs that are more complex and use a callback function. These can also be used in the same way. to see that, we can enumerate all resources in an executable. To do that, we load the executable with LoadLibraryEx. Once loaded, we use EnumerateResourceTypes to enumerate all resource types and, for every resource type, we enumerate the resources with EnumerateResourceNames.

Create a new project with

dotnet new console -o EnumerateResources

Then add the CsWin32 NuGet package with

dotnet add package Microsoft.Windows.CsWin32 --prerelease

Then, open the project in Visual Studio and add a new text file and name it NativeMethods.txt. Add these functions in the file:

LoadLibraryEx
FreeLibrary
EnumResourceTypes
EnumResourceNames

In Program.cs, erase all text and add this code:

using System;
using Microsoft.Windows.Sdk;

var hInst = PInvoke.LoadLibraryEx(@"C:\Windows\Notepad.exe",
    null, LoadLibraryEx_dwFlags.LOAD_LIBRARY_AS_DATAFILE);
try
{

}
finally
{
    PInvoke.FreeLibrary(hInst);
}

This code calls LoadLibraryEx to load Notepad. The LOAD_LIBRARY_AS_DATAFILE constant was changed to an enum. This function returns the Instance handle, that will be used to enumerate the resources. At the end, we free the instance using FreeLibrary.

Now, we’ll start to enumerate the resources with:

var hInst = PInvoke.LoadLibraryEx(@"C:\Windows\Notepad.exe",
    null, LoadLibraryEx_dwFlags.LOAD_LIBRARY_AS_DATAFILE);
try
{
    PInvoke.EnumResourceTypes(hInst, EnumerateTypes, 0);
}
finally
{
    PInvoke.FreeLibrary(hInst);
}

You can see that the second parameter in EnumerateResourceTypes is a callback function (which I don’t even know the signature :-)). Let’s see if Visual Studio will help us with this. We type Ctrl-. and it will propose us to create the method. We select it and it is created:

BOOL EnumerateTypes(nint hModule, PWSTR lpType, nint lParam)
{
    throw new NotImplementedException();
}

We can add our code to enumerate the types:

BOOL EnumerateTypes(nint hModule, PWSTR lpType, nint lParam)
{
    Console.WriteLine(PwStrToString(lpType));
    PInvoke.EnumResourceNames(hModule, lpType, EnumNames, 0);
    return true;
}

This code will write the resource type to the console and enumerate the resources for that type. We are using a function, PwStrToString to convert the resource name (a PWSTR) to a string:

string PwStrToString(PWSTR str)
{
    unsafe
    {
        return ((ulong)str.Value & 0xFFFF0000) == 0 ?
            ((ulong)str.Value).ToString() :
            str.AsSpan().ToString();
    }
}

This struct has a Value property, that can be an integer or a string. To know which of them we must use, we test against the high order byte and see if it’s empty. If it is, then the value is an integer and we convert it to a string. If not, we convert it to a Span and get the string from it. All this code must be marked as unsafe, as we are working with the pointers.

EnumerateResourceNames has a callback, which we get the signature in the same way we did before, by using Visual Studio refactoring:

BOOL EnumNames(nint hModule, PCWSTR lpType, PWSTR lpName, nint lParam)
{
    Console.WriteLine("  " + PwStrToString(lpName));
    return true;
}

Now the program is complete and we can run it to list all notepad’s resources:

As you can see, working with the Win32 API is much easier now, we don’t have to use custom P/Invokes, everything is at one place and all we have to do is to add the functions we want to the NativeMethods file. This is really a great and welcome improvement.

Ah, and if you want to know what those resource type numbers are, you can find them here.

  • 4 – Menu
  • 5 – Dialog
  • 6 – String
  • 9 – Accelerator
  • 16 – Version
  • 24 – Manifest

All the source code for this article is at https://github.com/bsonnino/UseWin32

 

With .NET 5.0, two small features were introduced to Asp.NET and were almost unnoticed: Open Api and HTTPRepl. Open Api is not something new, it’s been available for a long time, but it had to be included explicitly in a new project. Now, when you create a new project, it’s automatically included in the project and you can get the Api documentation using Swagger.

Now, when you create a new project with

dotnet new webapi

You will create a new WebApi project with a Controller, WeatherController, that shows 10 values of a weather forecast:

It’s a simple app, but it already comes with the OpenApi (Swagger) for documentation. Once you type the address:

https://localhost:5001/swagger

You will get the Swagger documentation and will be able to test the service:

But there is more. Microsoft introduced also HttpRepl, a REPL (Read-Eval-Print Loop) for testing REST services. It will scan your service and allow you to test the service using simple commands, like the file commands.

To test this new feature, in  new folder create a webapi app with

dotnet new webapi

Then, open Visual Studio Code with

code .

You will get something like that:

Then, delete the WeatherForecast.cs file and add a new folder and name it Model. In it, add a new file and name it Customer.cs and add this code:

public class Customer
{
    public string CustomerId { get; set; }
    public string CompanyName { get; set; }
    public string ContactName { get; set; }
    public string ContactTitle { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string Region { get; set; }
    public string PostalCode { get; set; }
    public string Country { get; set; }
    public string Phone { get; set; }
    public string Fax { get; set; }
}

Create a new file and name it CustomerRepository.cs and add this code:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;

namespace HttpRepl.Model
{
    public class CustomerRepository
    {
        private readonly IList<Customer> customers;

        public CustomerRepository()
        {
            var doc = XDocument.Load("Customers.xml");
            customers = new ObservableCollection<Customer>((from c in doc.Descendants("Customer")
                                                            select new Customer
                                                            {
                                                                CustomerId = GetValueOrDefault(c, "CustomerID"),
                                                                CompanyName = GetValueOrDefault(c, "CompanyName"),
                                                                ContactName = GetValueOrDefault(c, "ContactName"),
                                                                ContactTitle = GetValueOrDefault(c, "ContactTitle"),
                                                                Address = GetValueOrDefault(c, "Address"),
                                                                City = GetValueOrDefault(c, "City"),
                                                                Region = GetValueOrDefault(c, "Region"),
                                                                PostalCode = GetValueOrDefault(c, "PostalCode"),
                                                                Country = GetValueOrDefault(c, "Country"),
                                                                Phone = GetValueOrDefault(c, "Phone"),
                                                                Fax = GetValueOrDefault(c, "Fax")
                                                            }).ToList());
        }

        #region ICustomerRepository Members

        public bool Add(Customer customer)
        {
            if (customers.FirstOrDefault(c => c.CustomerId == customer.CustomerId) == null)
            {
                customers.Add(customer);
                return true;
            }
            return false;
        }

        public bool Remove(Customer customer)
        {
            if (customers.IndexOf(customer) >= 0)
            {
                customers.Remove(customer);
                return true;
            }
            return false;
        }

        public bool Update(Customer customer)
        {
            var currentCustomer = GetCustomer(customer.CustomerId);
            if (currentCustomer == null)
                return false;
            currentCustomer.CustomerId = customer.CustomerId;
            currentCustomer.CompanyName = customer.CompanyName;
            currentCustomer.ContactName = customer.ContactName;
            currentCustomer.ContactTitle = customer.ContactTitle;
            currentCustomer.Address = customer.Address;
            currentCustomer.City = customer.City;
            currentCustomer.Region = customer.Region;
            currentCustomer.PostalCode = customer.PostalCode;
            currentCustomer.Country = customer.Country;
            currentCustomer.Phone = customer.Phone;
            currentCustomer.Fax = customer.Fax;
            return true;    
        }

        public bool Commit()
        {
            try
            {
                var doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
                var root = new XElement("Customers");
                foreach (Customer customer in customers)
                {
                    root.Add(new XElement("Customer",
                                          new XElement("CustomerID", customer.CustomerId),
                                          new XElement("CompanyName", customer.CompanyName),
                                          new XElement("ContactName", customer.ContactName),
                                          new XElement("ContactTitle", customer.ContactTitle),
                                          new XElement("Address", customer.Address),
                                          new XElement("City", customer.City),
                                          new XElement("Region", customer.Region),
                                          new XElement("PostalCode", customer.PostalCode),
                                          new XElement("Country", customer.Country),
                                          new XElement("Phone", customer.Phone),
                                          new XElement("Fax", customer.Fax)
                                 ));
                }
                doc.Add(root);
                doc.Save("Customers.xml");
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        public IEnumerable<Customer> GetAll() => customers;

        public Customer GetCustomer(string id) => customers.FirstOrDefault(c => string.Equals(c.CustomerId, id, StringComparison.CurrentCultureIgnoreCase));

        #endregion

        private static string GetValueOrDefault(XContainer el, string propertyName)
        {
            return el.Element(propertyName) == null ? string.Empty : el.Element(propertyName).Value;
        }
    }
}

This code will use a file, named Customers.xml and will use it to serve the customers repository. With it, you will be able to get all customers, get, add, update or delete one customer. We will use it to serve our controller. The Customers.xml can be obtained at . You should add this file to the main folder and then add this code:

<ItemGroup >
  <None Update="customers.xml" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

This will ensure that the xml file is copied to the output folder when the project is built.

Then, in the Controllers folder, delete the WeatherForecastController and add a new file, CustomerController.cs and add this code:

using System.Collections.Generic;
using HttpRepl.Model;
using Microsoft.AspNetCore.Mvc;

namespace HttpRepl.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CustomerController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<Customer> Get()
        {
            var customerRepository = new CustomerRepository();
            return customerRepository.GetAll();
        }

        [HttpGet("{id}")]
        public IActionResult GetCustomer(string id)
        {
            var customerRepository = new CustomerRepository();
            Customer customer = customerRepository.GetCustomer(id);
            return customer != null ? Ok(customer) : NotFound();
        }

        [HttpPost]
        public IActionResult Add([FromBody] Customer customer)
        {
            if (string.IsNullOrWhiteSpace(customer.CustomerId))
              return BadRequest();
            var customerRepository = new CustomerRepository();
            if (customerRepository.Add(customer))
            {
                customerRepository.Commit();
                return CreatedAtAction(nameof(Get), new { id = customer.CustomerId }, customer);
            }
            return Conflict();
        }

        [HttpPut]
        public IActionResult Update([FromBody] Customer customer)
        {
            if (string.IsNullOrWhiteSpace(customer.CustomerId))
              return BadRequest();
            var customerRepository = new CustomerRepository();
            var currentCustomer = customerRepository.GetCustomer(customer.CustomerId);
            if (currentCustomer == null)
                return NotFound();
            if (customerRepository.Update(customer))
            {
                customerRepository.Commit();
                return Ok(customer);
            }
            return NoContent();
        }

        [HttpDelete("{id}")]
        public IActionResult Delete([FromRoute]string id)
        {
            if (string.IsNullOrWhiteSpace(id))
              return BadRequest();
            var customerRepository = new CustomerRepository();
            var currentCustomer = customerRepository.GetCustomer(id);
            if (currentCustomer == null)
                return NotFound();
            if (customerRepository.Remove(currentCustomer))
            {
                customerRepository.Commit();
                return Ok();
            }
            return NoContent();
        }
    }
}

This controller will add actions to get all customers, get add, update or delete one customer. The project is ready to be run. You can run it with dotnet run and, when you open a new browser window and type this address:

https://localhost:5001/swagger

You will get something like this:

You can test the service with the Swagger page (as you can see, it was generated automatically wen you compiled and ran the app), but there is still another tool: HttpRepl. This tool was added with .NET 5 and you can install it with the command:

dotnet tool install -g Microsoft.dotnet-httprepl

Once you install it, you can run it with

httprepl https://localhost:5001

When you run it, you will get the REPL prompt:

If you type the uicommand, a new browser window will open with the Swagger page. You can type lsto list the available controllers and actions:

As you can see, it has a folder structure and you can test the service using the command line. For example, you can get all the customers with get customer or get one customer with get customer/blonp:

You can also “change directory” to the Customer “directory” with cd Customer. In this case, you can query the customer with get blonp:

If the body content is simple, you can use the -c parameter, like in:

post -c "{"customerid":"test"}"

This will add a new record with just the customer id:

If the content is more complicated, you must set the default editor, so you can edit the customer that will be inserted in the repository. You must do that with the command:

pref set editor.command.default "c:\windows\system32\notepad.exe"

This will open notepad when you type a command that needs a body, so you can type the body that will be sent when the command is executed. If you type post in the command line, notepad will open to edit the data. You can type this data:

{
  "customerId": "ABCD",
  "companyName": "Abcd Inc.",
  "contactName": "A.B.C.Dinc",
  "contactTitle": "Owner",
  "address": "1234 - Acme St - Suite A",
  "city": "Abcd",
  "region": "AC",
  "postalCode": "12345",
  "country": "USA",
  "phone": "(501) 555-1234"
}

When you save the file and close notepad, you will get something like this:

If you try to add the same record again, you will get an error 409, indicating that the record already exists in the database:

As you can see, the repository is doing the checks and sending the correct response to the REPL. You can use the same procedure to update or delete a customer. For the delete, you just have to pass the customer Id:

Now that we know all the commands we can do with the REPL, we can go one step further: using scripts. You can write a text file with the commands to use and run the script. Let’s say we want to exercise the entire API, by issuing all commands in a single run. We can create a script like this (we’ll name it TestApi.txt)

connect https://localhost:5001
ls
cd Customer
ls
get 
get alfki
post --content "{"customerId": "ABCD","companyName": "Abcd Inc.","contactName": "A.B.C.Dinc"}"
get abcd
put --content "{"customerId": "ABCD","companyName": "ACME Inc"}"
delete abcd
get abcd
cd ..
ls

And then open HttpRepl and run the command

run testapi.txt

We’ll get this output:

As you can see, with this tool, you get a very easy way to exercise your API. It’s not something fancy as a dedicated program like Postman, but it’s easy to use and does its job.

The full code for the project is at https://github.com/bsonnino/HttpRepl

 

In the last post I’ve shown how to use the new WebView2 control in a WPF app and said that it could be used in any Windows version and in any platform. As a matter of fact, I can create a 64 bit VCL native application with Delphi, that uses the Win32 API with the same control, offering the same functionality that the previous app did. To do that, you must have Delphi 10.4 Sydney installed in your machine. It offers the TEdgeBrowser component, that can be used to browse the web using the Chromium component.

In Delphi, create a new VCL application. Add the 64 bit platform to it and set it default. Add a TPanel, docked at the top. Then add a TEdgeBrowser and doc it to fill the window. In the panel, add a TLabel and change the Caption property to Search Text, add a TEdit and clear its Text property. Then, add two buttons and set their property Caption to Find and Copy. Add two more buttons at the right of the panel and set their sizes to 23×23, and their font Height to 10, Name to Segoe MDL2 Assets and their caption to the Back and Forward icons in the Character Map:

You should have something like this:

Now you can set the OnCreate event of the main form to

EdgeBrowser1.Navigate('https://docs.microsoft.com');

and run the application, but you won’t see nothing in the window. You can check the initialization of the WebViewer in the OnCreatedWebViewCompleted event and check the AResult parameter.

procedure TForm1.EdgeBrowser1CreateWebViewCompleted(Sender: TCustomEdgeBrowser;
  AResult: HRESULT);
begin
  if AResult <> 0 then
    ShowMessage('Error initializing WebView: $'+IntToHex(AResult));
end;

You will see that there is an error code (in my machine it’s $80004005). This is due to the fact that the WebView dll is missing. The dll is in the Redist folder and you have to add a PostBuild command to the project. Go to Project/Options and select Build options and, in the command “Value from All Configurations – Windows 64 bits”, add the following command:

copy /Y "C:\Program Files (x86)\Embarcadero\Studio\21.0\Redist\win64\WebView2Loader.dll" $(OUTPUTDIR)

This will copy the dll to the output directory and will initialize the WebView correctly(you will see that there is no message in the OnCreatedWebViewCompleted event and the page is loaded. Now, we can add the event handlers for the buttons. The Find button will set up the address and call the Navigate method (You must add the NetEncoding unit to the Uses clause):

procedure TForm1.FindClick(Sender: TObject);
begin
  if Edit1.Text <> '' then begin
    var address := 'https://docs.microsoft.com/en-us/search/?terms=' +
      TNetEncoding.URL.Encode(Edit1.Text);
    EdgeBrowser1.Navigate(address);
  end;
end;

The Back and Forward buttons events are very simple to implement:

procedure TForm1.BackClick(Sender: TObject);
begin
  if EdgeBrowser1.CanGoBack then
    EdgeBrowser1.GoBack;
end;

procedure TForm1.ForwardClick(Sender: TObject);
begin
  if EdgeBrowser1.CanGoForward then
    EdgeBrowser1.GoForward;
end;

To remove the parts of the page that we don’t want, we use the OnNavigationCompleted event to inject the JavaScript code:

procedure TForm1.EdgeBrowser1NavigationCompleted(Sender: TCustomEdgeBrowser;
  IsSuccess: Boolean; WebErrorStatus: TOleEnum);
begin
  if IsSuccess then
    EdgeBrowser1.ExecuteScript(
' var rss = document.querySelector(''[data-bi-name="search-rss-link"]'');'+ #13#10 +
' console.log(rss);'+ #13#10 +
' if (rss)'+ #13#10 +
'   rss.style.display = "none";'+ #13#10 +
' var form = document.getElementById("facet-search-form");'+ #13#10 +
' console.log(form);'+ #13#10 +
' if (form)'+ #13#10 +
'   form.style.display = "none";'+ #13#10 +
' var container = document.getElementById("left-container");'+ #13#10 +
' console.log(container);'+ #13#10 +
' if (container)'+ #13#10 +
'   container.style.display = "none";'+ #13#10 +
' var hiddenClasses = ["header-holder", "footerContainer"];'+ #13#10 +
' var divs = document.getElementsByTagName("div");'+ #13#10 +
' for( var i = 0; i < divs.length; i++) {'+ #13#10 +
'   if (hiddenClasses.some(r=> divs[i].classList.contains(r))){'+ #13#10 +
'     divs[i].style.display = "none";'+ #13#10 +
'   }'+ #13#10 +
' }');
end;

You can see here the same JavaScript code we used in the previous post, it also logs to the console the value of the variables. You can also open the Developer Tools of the browser component by pressing F12.

Now, there is only the Copy button to get the results and copy them to the clipboard. We will inject the code that gets all the results and send them to the application:

procedure TForm1.CopyClick(Sender: TObject);
begin
EdgeBrowser1.ExecuteScript(
'var results = [...document.querySelectorAll(''[data-bi-name="result"]'')]'+
'.map(a => {'+ #13#10 +
' let aElement = a.querySelector("a");'+ #13#10 +
' return {'+ #13#10 +
' title: aElement.innerText,'+ #13#10 +
' link: aElement.getAttribute("href")'+ #13#10 +
' };'+ #13#10 +
'});'+ #13#10 +
'console.log(results);'+ #13#10 +
'if (results.length >= 1){'+ #13#10 +
' window.chrome.webview.postMessage(results);'+ #13#10 +
'}'+ #13#10 +
'else {'+ #13#10 +
' alert("There are no results in the page");'+ #13#10 +
'}');
end;

This is the same code that is injected in the WPF app, with a slight difference: in the WPF program we’ve sent a message and added a listener in the JavaScript code. Here, we are running the code when the user clicks the button. This will send a message to the app, that will be processed in the OnMessageReceived event:

procedure TForm1.EdgeBrowser1WebMessageReceived(Sender: TCustomEdgeBrowser;
  Args: TWebMessageReceivedEventArgs);
var
  json : PWideChar;
begin
  var msg := Args as ICoreWebView2WebMessageReceivedEventArgs;
  msg.Get_webMessageAsJson(json);
  Clipboard.AsText := json;
  ShowMessage('Results sent to clipboard');
end

Now, when you run the program, you will have the same results as in the WPF program:

As you can see, with the new WebView2 component you have a lot of flexibility, you can use it in .NET or Win32 programs with almost no change. You can use this component to browse the Web and get data from the browsing, or you can use it to complement your current app: let’s say you have parts of your app that are already written for the web and you don’t want to rewrite them, but use them to interact with your app, you can add these parts and include them in your desktop app.

The full source code for this project is at https://github.com/bsonnino/WebViewDelphi

 

 

Times have changed and Microsoft is not the same: Edge, the new Microsoft browser has been remodeled and now it’s using the Chromium engine, an open source browser engine developed by Google.

With that, it has also changed the way you can develop browser apps – you will be able to use the same browser engine Microsoft uses in its browser to develop your browser apps. In order to do that, you will have to use the new WebView control. The new WebView control is not tied to a specific Windows version or development platform: You can use it in any Windows version, from 7 to 10 or use it in a Win32, .NET (core or full framework) or UWP app.

This article will show how to use it and interact with it in a WPF app. We will develop a program that will search in the Microsoft docs site, so you will be able to easily search there for information.

Introduction

Using the new WebView2 control in a .NET app is very simple, it’s just a matter of adding a NuGet package and you’re already setup. In Visual Studio, create a new WPF app. Right click the dependencies node in the Solution Explorer and select “Manage NuGet Packages” , the select the Microsoft.Web.WebView2 package. Then, in MainWindow.xaml, add the main UI:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="40"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Search Text" Margin="5" VerticalAlignment="Center"/>
        <TextBox Text="" x:Name="SearchText" Margin="5" VerticalAlignment="Center"
                 Width="400" Height="30" VerticalContentAlignment="Center"/>
        <Button Content="Find" Width="65" Height="30" Margin="5" Click="ButtonBase_OnClick"/>
    </StackPanel>
    <wpf:WebView2 x:Name="WebView" Grid.Row="1" Source="" />
</Grid>

You will have to add the wpf  namespace to the xaml:

xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"

As you can see, we are adding a textbox for the text to search and a button to activate the search. In the WebView control, we left the Source property blank, as we don’t want to go to a specific site. If we wanted to start with some page, we would have to fill this property. For example, if you fill the Source property with  https://docs.microsoft.com you would have something like this:

 

Navigating with the WebView

As you can see, it’s very easy to add a browser to your app, but we want to add more than a simple browsing experience. We want to make our app a custom way of browsing. To do that, we will use the following button click event handler:

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    if (string.IsNullOrWhiteSpace(SearchText.Text))
        return;
    var searchAddress =
        $"https://docs.microsoft.com/en-us/search/?terms={HttpUtility.UrlEncode(SearchText.Text)}";
    WebView?.CoreWebView2?.Navigate(searchAddress);
}

We’ll take the text the user will want to search, create an URL for searching in the Microsoft docs website and then navigate to it. Once we do that, we can search the docs, just by typing the wanted text and clicking the button:

We can also add back and forward navigation by adding these two buttons:

<StackPanel Orientation="Horizontal" Grid.Row="0" HorizontalAlignment="Right" TextElement.FontFamily="Segoe MDL2 Assets">
    <Button Content="&amp;#xE0A6;" Width="30" Height="30" Margin="5" ToolTip="Back" Click="GoBackClick"/>
    <Button Content="&amp;#xE0AB;" Width="30" Height="30" Margin="5" ToolTip="Forward" Click="GoForwardClick"/>
</StackPanel>

The click event handler that will allow the browser to navigate to the previous or next page in history is:

private void GoBackClick(object sender, RoutedEventArgs e)
{
    if (WebView.CanGoBack)
        WebView.GoBack();
}

private void GoForwardClick(object sender, RoutedEventArgs e)
{
    if (WebView.CanGoForward)
        WebView.GoForward();
}

Once you have this code in place, you are able to use the two buttons to navigate in the browser history.

Customizing the page

One thing that bothers me in this app is the fact that we have several items in the page that don’t belong to our search: the top bar, the footer bar, the search box, and so on. Wouldn’t it be nice to clean these items from the page when we are showing it? Well, there is a way to do that, but we’ll have to resort to JavaScript to do that: when the page is loaded, we’ll inject a JavaScript script in the page that will remove the parts we don’t want. For that, we must create this code:

        async void InitializeAsync()
        {
            await WebView.EnsureCoreWebView2Async(null);
            WebView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
        }

        private async void CoreWebView2_DOMContentLoaded(object sender, CoreWebView2DOMContentLoadedEventArgs e)
        {

            await WebView.ExecuteScriptAsync(
                @"
window.onload = () => {
  var rss = document.querySelector('[data-bi-name=""search-rss-link""]');
  console.log(rss);
  if (rss)
    rss.style.display = 'none'; 
  var form = document.getElementById('facet-search-form');
  console.log(form);
  if (form)
    form.style.display = 'none';  
  var container = document.getElementById('left-container');
  console.log(container);
  if (container)
    container.style.display = 'none';  
  var hiddenClasses = ['header-holder', 'footerContainer'];
  var divs = document.getElementsByTagName('div');
  for( var i = 0; i < divs.length; i++) {
    if (hiddenClasses.some(r=> divs[i].classList.contains(r))){
      divs[i].style.display = 'none';
    }
  }
}");
        }

We have two parts in this code: initially, we ensure that the CoreWebView2 component is created and then we set a DomContentLoaded event handler, that will be called when the HTML content is loaded in the browser. In the event handler, we will inject the script and execute it with ExecuteScriptAsync. That is enough to remove the parts we don’t want from the page. The JavaScript code will retrieve the parts we want to hide and set their display style to none. This code is called from the constructor of the main window:

public MainWindow()
{
    InitializeComponent();
    InitializeAsync();
}

 

You can also see the console.log commands in the code. You can debug the JavaScript code when browsing by using the F12 key. The developer window will open in a separate window:

As you can see from the image, the top bar was removed, we now have a cleaner page. You can use the same technique to remove the context menu or add other functionality to the browser. Now we will get some extra info from the page.

Communicating between the app and the browser

When we are browsing the results page, we can get something from it. We can copy the results to the clipboard, so we can use them later. To do that we must use the communication between the app and the WebView. This is done by a messaging process. The app can send a message to the WebView, using something like

WebView?.CoreWebView2?.PostWebMessageAsString("message");

The WebView will receive the message and can process it by adding an event listener like in

window.chrome.webview.addEventListener('message', event => {
  if (event.data === 'message') {
    // process message
  }
});

When we want to send messages in the other direction, from the WebView to the app, we can send it from JavaScript, using

window.chrome.webview.postMessage(message);

It will be received by the app with an event handler like

WebView.CoreWebView2.WebMessageReceived += (s, args) =>
{
  data = args.WebMessageAsJson;
  // Process data
}

That way, we can have full communication between the app and the WebView and we can add the functionality we want: copy the results from the results page to the clipboard. The first step is to add the button to copy the results in MainWindow.xaml:

<StackPanel Orientation="Horizontal">
    <TextBlock Text="Search Text" Margin="5" VerticalAlignment="Center"/>
    <TextBox Text="" x:Name="SearchText" Margin="5" VerticalAlignment="Center"
             Width="400" Height="30" VerticalContentAlignment="Center"/>
    <Button Content="Find" Width="65" Height="30" Margin="5" Click="ButtonBase_OnClick"/>
    <Button Content="Copy" Width="65" Height="30" Margin="5" Click="CopyClick"/>
</StackPanel>

The click event handler will send a message to the WebView:

private void CopyClick(object sender, RoutedEventArgs e)
{
    WebView?.CoreWebView2?.PostWebMessageAsString("copy");
}

We must inject some code in the web page to receive and process the message, this is done by using the AddScriptToExecuteOnDocumentCreatedAsync method to inject the code when the page is loaded, in the InitializeAsync method:

        async void InitializeAsync()
        {

            await WebView.EnsureCoreWebView2Async(null);
            WebView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
            await WebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(@"
window.chrome.webview.addEventListener('message', event => {
  if (event.data === 'copy') {
    var results = [...document.querySelectorAll('[data-bi-name=""result""]')].map(a => {
            let aElement = a.querySelector('a');
            return {
                title: aElement.innerText,
                link: aElement.getAttribute('href')
            };
        });
    if (results.length >= 1){
      window.chrome.webview.postMessage(results);
    }
    else {
      alert('There are no results in the page');
    }
  }
});");
            WebView.CoreWebView2.WebMessageReceived += DataReceived;
        }

The JavaScript code will add the event listener, that will get all results in the page using the querySelectorAll  method, and then it will map it to an array of objects that have the title and link of the result, then will send this array to the app with postMessage. In the case that there are no results in the page, an alert message is shown. The code also sets the event handler for the WebMessageReceived event:

void DataReceived(object sender, CoreWebView2WebMessageReceivedEventArgs args)
{
    var data = args.WebMessageAsJson;
    Clipboard.SetText(data);
    MessageBox.Show("Results copied to the clipboard");
}

The handler will get the sent data with the args.WebMessageAsJson, then it will send it to the clipboard as text, where it can be copied to any program. Now, when you run the program, do a search and click the Copy  button, you will have something like this in the clipboard:

[
    {
        "title": "Getting started with WebView2 for WinForms apps - Microsoft Edge Development",
        "link": "https://docs.microsoft.com/en-us/microsoft-edge/webview2/gettingstarted/winforms"
    },
    {
        "title": "Microsoft Edge WebView2 Control - Microsoft Edge Development",
        "link": "https://docs.microsoft.com/en-us/microsoft-edge/webview2/"
    },
    {
        "title": "Getting started with WebView2 for WinUI apps - Microsoft Edge Development",
        "link": "https://docs.microsoft.com/en-us/microsoft-edge/webview2/gettingstarted/winui"
    },
    {
        "title": "Getting started with WebView2 for WPF apps - Microsoft Edge Development",
        "link": "https://docs.microsoft.com/en-us/microsoft-edge/webview2/gettingstarted/wpf"
    },
    {
        "title": "Versioning of Microsoft Edge WebView2 - Microsoft Edge Development",
        "link": "https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/versioning"
    },
    {
        "title": "Distribution of Microsoft Edge WebView2 apps - Microsoft Edge Development",
        "link": "https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution"
    },
    {
        "title": "Getting started with WebView2 for Win32 apps - Microsoft Edge Development",
        "link": "https://docs.microsoft.com/en-us/microsoft-edge/webview2/gettingstarted/win32"
    },
    {
        "title": "Release Notes for Microsoft Edge WebView2 for Win32, WPF, and WinForms - Microsoft Edge Development",
        "link": "https://docs.microsoft.com/en-us/microsoft-edge/webview2/releasenotes"
    },
    {
        "title": "Use JavaScript in WebView2 apps - Microsoft Edge Development",
        "link": "https://docs.microsoft.com/en-us/microsoft-edge/webview2/howto/js"
    },
    {
        "title": "Microsoft Edge WebView2 API Reference - Microsoft Edge Development",
        "link": "https://docs.microsoft.com/en-us/microsoft-edge/webview2/webview2-api-reference"
    }
]

Now we have an app that can browse to a page and interact with it.

Conclusion

There are many uses to this feature and the new Chromium WebView is a welcome addition to our toolbox, we can interact with the web page, retrieving and sending data to it.

The full source code for this article is at https://github.com/bsonnino/WebViewWpf

Introduction

You have an old, legacy app (with no tests), and its age is starting to show – it’s using an old version of the .NET Framework, it’s difficult to maintain and every new feature introduced brings a lot of bugs. Developers are afraid to change it, but the users ask for new features. You are at a crossroad: throw the code and rewrite everything or refactor the code. Does this sound familiar to you ?

I’m almost sure that you’re leaning to throw the code and rewrite everything. Start fresh, use new technologies and create the wonderful app you’ve always dreamed of. But this comes with a cost: the old app is still there, functional, and must be maintained while you are developing the new one. There are no resources to develop both apps in parallel and the new app will take a long time before its finished.

So, the only way to go is to refactor the old app. It’s not what you wanted, but it can still be fun – you will be able to use the new technologies, introduce good programming practices, and at the end, have the app you have dreamed. No, I’m not saying it will be an easy way, but it will be the most viable one.

This article will show how to port a .NET 4 WPF app and port it to .NET 5, introduce the MVVM pattern and add tests to it. After that, you will be able to change its UI, using WinUI3, like we did in this article.

The original app

The original app is a Customer CRUD, developed in .NET 4, with two projects – the UI project, CustomerApp, and a library CustomerLib, that access client’s data in an XML file (I did that just for the sake of simplicity, but this could be changed easily for another data source, like a database). You can get the app from here, and when you run it, you get something like this:

The first step will be converting it to .NET 5. Before that, we will see how portable is our app, using the .NET Portability analyzer. It’s a Visual studio extension that you can download from here. Once you download and install it, ou can run it in Visual Studio with Analyze/Portability Analyzer Settings:

You must select the platforms you want and click OK. Then, you must select Analyze/Analyze Assembly Portability, select the executables for the app and click OK. That will generate an Excel file with the report:

As you can see, our app can be ported safely to .NET 5. If there are any problems, you can check them in the Details tab. There, you will have a list of all APIs that you won’t be able to port, where you should find a workaround. Now, we’ll start converting the app.

Converting the app to .NET 5

To convert the app, we’ll start converting the lib to .NET Standard 2.0. To do that, right-click in the Lib project in the Solution Explorer and  select Unload Project, then edit the project file and change it to:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0</TargetFrameworks>
  </PropertyGroup>
  <ItemGroup>
    <Content Include="Customers.xml">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
  </ItemGroup>
</Project>

The project file is very simple, just set the taget framework to netstandard2.0 and copy the item group relative to the xml file, so it’s included in the final project. Then, you must reload the project and remove the AssemblyInfo  file from the Properties folder, as it isn’t needed anymore (if you leave it, it will generate an error, as it’s included automatically by the new project file).

Then, right click the app project in the Solution Explorer and select Unload Project, to edit the project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\CustomerLib\CustomerLib.csproj">
      <Name>CustomerLib</Name>
    </ProjectReference>
  </ItemGroup>
</Project>

We are setting the output type to WinExe, setting the target framework to net5.0-windows, telling that we will use the .net 5 features, plus the ones specific to Windows. If we don’t do that, we wouldn’t be able to use the WPF features, that are specific to Windows and set UseWPF to true. Then, we copy the lib’s ItemGroup to the project. When we reload the project, we must delete the Properties folder and we can build our app, converted to .NET 5. It should work exactly the way it did before. We are in the good path, porting the app to the newest .NET version, but we still have a long way to go. Now it’s time to add good practices to our app, using the MVVM Pattern.

The MVVM Pattern

The MVVM (Model-View-ViewModel) pattern was created on 2005 by John Gossman, a Microsoft Architect on Blend team, and it makes extensive use of the DataBinding feature existent in WPF and other XAML platforms (like UWP or Xamarin). It provides separation between data (Model) and its visualization (View), using a binding layer, the ViewModel.

The  ViewModel  is a class that implements the INotifyPropertyChanged interface:

public interface INotifyPropertyChanged 
{ 
  event PropertyChangedEventHandler PropertyChanged; 
}

It has just one event, PropertyChanged that is activated when there is a change in a property. The Data binding mechanism present in WPF (and in other XAML platforms) subscribes this event and updates the view with no program intervention. So, all we need to do is to create a class that implements INotifyPropertyChanged and call this event when there is a change in a property to WPF update the view.

The greatest advantage is that the ViewModel is a normal class and doesn’t have any dependency on the view layer. That way, we don’t need to initialize a window when we test the ViewModel. This image shows the basic structure of this pattern:

The model communicates with the ViewModel by its properties and methods. The ViewModel communicates with the View mainly using Data Binding, it receives Commands from the View and can send messages to it. When there are many ViewModels that must communicate , they usually send messages, to maintain a decoupled architecture. That way, the Model (usually a POCO class – Plain Old CSharp Object) doesn’t know about the ViewModel, the ViewModel isn’t coupled with the View or other ViewModels, and the View isn’t tied to a ViewModel directly (the only tie is the View’s DataContext property, that will bind the View and the ViewModel. The rest will be done by Data Binding).

We could implement all this infrastructure by ourselves, it’s not a difficult task, but it’s better to use a Framework for that. There are many MVVM frameworks out there, each one chooses a different approach to implement the infrastructure and select one of them is just a matter of preference. I’ve used MVVM Light toolkit for years, it’s very lightweight and easy to use, but it isn’t maintained anymore, so I decided to search another framework. Fortunately, the Windows Community Toolkit has provided a new framework, inspired on MVVM Light, the MVVM Community Toolkit.

Implementing the MVVM Pattern

In our project, the Model is already separated from the rest: it’s in the Lib project and we’ll leave that untouched, as we don’t have to change anything in the model. In order to use the MVVM Toolkit, we must add the Microsoft.Toolkit.Mvvm NuGet package.

Then, we must create the ViewModels to interact between the View and the Model. Create a new folder and name it ViewModel. In it, add a new class and name it MainViewModel.cs.  This class will inherit from ObservableObject, from the toolkit, as it already implements the interface. Then we will copy and adapt the code that is found in the code behind of MainWindow.xaml.cs:

 

public class MainViewModel : ObservableObject
{
    private readonly ICustomerRepository _customerRepository;
    private Customer _selectedCustomer;

    public MainViewModel()
    {
        _customerRepository =  new CustomerRepository();
        AddCommand = new RelayCommand(DoAdd);
        RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
        SaveCommand = new RelayCommand(DoSave);
        SearchCommand = new RelayCommand<string>(DoSearch);
    }

    public IEnumerable<Customer> Customers => _customerRepository.Customers;

    public Customer SelectedCustomer
    {
        get => _selectedCustomer;
        set 
        { 
            SetProperty(ref _selectedCustomer, value); 
            RemoveCommand.NotifyCanExecuteChanged(); 
        } 
    }

    public IRelayCommand AddCommand { get; }
    public IRelayCommand RemoveCommand { get; }
    public IRelayCommand SaveCommand { get; }
    public IRelayCommand<string> SearchCommand { get; }
    private void DoAdd()
    {
        var customer = new Customer();
        _customerRepository.Add(customer);
        SelectedCustomer = customer;
        OnPropertyChanged("Customers");
    }

    private void DoRemove()
    {
        if (SelectedCustomer != null)
        {
            _customerRepository.Remove(SelectedCustomer);
            SelectedCustomer = null;
            OnPropertyChanged("Customers");
        }
    }

    private void DoSave()
    {
        _customerRepository.Commit();
    }

    private void DoSearch(string textToSearch)
    {
        var coll = CollectionViewSource.GetDefaultView(Customers);
        if (!string.IsNullOrWhiteSpace(textToSearch))
            coll.Filter = c => ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower());
        else
            coll.Filter = null;
    }
}

We have two properties, Customers and SelectedCustomerCustomers  will contain the list of customers shown in the DataGrid. SelectedCustomer will be the selected customer in the DataGrid, that will be shown in the detail pane. There are four commands, and we will use the IRelayCommand interface, declared in the toolkit. Each command will be initialized with the method that will be executed when the command is invoked. The RemoveCommand uses an overload for the constructor, that uses a predicate as the second parameter. This predicate will only enable the button when there is a customer selected in the DataGrid. As this command is dependent on the selected customer, when we change this property, we call the NotifyCanExecuteChanged method to notify all the elements that are bound to this command.

Now we can remove all the code from MainWindow.xaml.cs and leave only this:

public MainWindow()
{
    InitializeComponent();
    DataContext = new MainViewModel();
}

We can run the program and see that it runs the same way it did before, but we made a large refactoring to the code and now we can start implementing unit tests in the code.

Implementing tests

Now that we’ve separated the code from the view, we can test the ViewModel without the need to initialize a Window. That is really great, because we can have testable code and be assured that we are not breaking anything when we are implementing new features. For that, add a new test project and name it CustomerApp.Tests. In the Visual Studio version I’m using, there is no template for the .net 5.0 test project available, so I added a .Net Core test project, then I edited the project file and changed the TargetFramework to net5.0-windows. Then, you can add a reference to the CustomerApp project and rename UnitTest1 to MainViewModelTests.

Taking a look at the Main ViewModel, we see that there is a coupling between it and the Customer Repository. In this case, there is no much trouble, because we are reading the customers from a XML file located in the output directory, but if we decide to replace it with some kind of database, it can be tricky to test the ViewModel, because we would have to do a lot of setup to test it.

We’ll remove the dependency using Dependency Injection. Instead of using another framework for the the dependency injection, we’ll use the integrated one, based on Microsoft.Extensions.DependencyInjection. You should add this NuGet package in the App project to use the dependency injection. Then, in App.xaml.cs, we’ll add code to initialize the location of the services:

public partial class App
{
    public App()
    {
        Services = ConfigureServices();
    }

    public new static App Current => (App) Application.Current;

    public IServiceProvider Services { get; }

    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        services.AddSingleton<ICustomerRepository, CustomerRepository>();
        services.AddSingleton<MainViewModel>();

        return services.BuildServiceProvider();
    }

    public MainViewModel MainVM => Services.GetService<MainViewModel>();
}

We declare a static property Current to ease using the App  object and declare a IServiceProvider, to provide our services. They are configured in the ConfigureServices method, that creates a ServiceCollection and add the CustomerRepository and the main ViewModel to the collection. ConfigureServices  is called in the constructor of the application. Finally we declare the property MainVM, which will get the ViewModel from the Service Collection.

Now, we can change MainWindow.xaml.cs to use the property instead of instantiate directly the ViewModel:

public MainWindow()
{
    InitializeComponent();
    DataContext = App.Current.MainVM;
}

The last change is to remove the coupling between the ViewModel and the repository using Dependency Injection, in MainViewModel.cs:

public MainViewModel(ICustomerRepository customerRepository)
{
    _customerRepository = customerRepository ??  
                          throw new ArgumentNullException("customerRepository");
    _customerRepository = customerRepository;
    AddCommand = new RelayCommand(DoAdd);
    RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
    SaveCommand = new RelayCommand(DoSave);
    SearchCommand = new RelayCommand<string>(DoSearch);
}

With that, we’ve gone one step further and removed the coupling between the ViewModel and the repository, so we can start our tests.

For the tests, we will use two libraries, FluentAssertions, for better assertions and FakeItEasy, to generate fakes. You should install both NuGet packages to your test project. Now, we can start creating our tests:

public class MainViewModelTests
{
    [TestMethod]
    public void Constructor_NullRepository_ShouldThrow()
    {
        Action act = () => new MainViewModel(null);

        act.Should().Throw<ArgumentNullException>()
            .Where(e => e.Message.Contains("customerRepository"));
    }

    [TestMethod]
    public void Constructor_Customers_ShouldHaveValue()
    {
        var repository = A.Fake<ICustomerRepository>();
        var customers = new List<Customer>();
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm =  new MainViewModel(repository);

        vm.Customers.Should().BeEquivalentTo(customers);
    }

    [TestMethod]
    public void Constructor_SelectedCustomer_ShouldBeNull()
    {
        var repository = A.Fake<ICustomerRepository>();
        var vm = new MainViewModel(repository);

        vm.SelectedCustomer.Should().BeNull();
    }
}

Here we created three tests for the constructor, testing the values of the properties after the constructor. We can continue, testing the commands in the ViewModel:

[TestMethod]
public void AddCommand_ShouldAddInRepository()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);

    vm.AddCommand.Execute(null);
    A.CallTo(() => repository.Add(A<Customer>._)).MustHaveHappened();
}

[TestMethod]
public void AddCommand_SelectedCustomer_ShouldNotBeNull()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.AddCommand.Execute(null);
    vm.SelectedCustomer.Should().NotBeNull();
}

[TestMethod]
public void AddCommand_ShouldNotifyCustomers()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    var wasNotified = false;
    vm.PropertyChanged += (s, e) =>
    {
        if (e.PropertyName == "Customers")
            wasNotified = true;
    };
    vm.AddCommand.Execute(null);
    wasNotified.Should().BeTrue();
}

[TestMethod]
public void RemoveCommand_SelectedCustomerNull_ShouldNotRemoveInRepository()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.RemoveCommand.Execute(null);
    A.CallTo(() => repository.Remove(A<Customer>._)).MustNotHaveHappened();
}

[TestMethod]
public void RemoveCommand_SelectedCustomerNotNull_ShouldRemoveInRepository()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SelectedCustomer = new Customer();
    vm.RemoveCommand.Execute(null);
    A.CallTo(() => repository.Remove(A<Customer>._)).MustHaveHappened();
}

[TestMethod]
public void RemoveCommand_SelectedCustomer_ShouldBeNull()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SelectedCustomer = new Customer();
    vm.RemoveCommand.Execute(null);
    vm.SelectedCustomer.Should().BeNull();
}

[TestMethod]
public void RemoveCommand_ShouldNotifyCustomers()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SelectedCustomer = new Customer(); 
    var wasNotified = false;
    vm.PropertyChanged += (s, e) =>
    {
        if (e.PropertyName == "Customers")
            wasNotified = true;
    };
    vm.RemoveCommand.Execute(null);
    wasNotified.Should().BeTrue();
}

[TestMethod]
public void SaveCommand_ShouldCommitInRepository()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SaveCommand.Execute(null);
    A.CallTo(() => repository.Commit()).MustHaveHappened();
}

[TestMethod]
public void SearchCommand_WithText_ShouldSetFilter()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SearchCommand.Execute("text");
    var coll = CollectionViewSource.GetDefaultView(vm.Customers);
    coll.Filter.Should().NotBeNull();
}

[TestMethod]
public void SearchCommand_WithoutText_ShouldSetFilter()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SearchCommand.Execute("");
    var coll = CollectionViewSource.GetDefaultView(vm.Customers);
    coll.Filter.Should().BeNull();
}

Now we have all the tests for the ViewModel and have our project ready for the future. We went step by step and finished with a .NET 5.0 project that uses the MVVM pattern and have unit tests. This project is ready to be updated to WinUI3, or even to be ported to UWP or Xamarin. The separation between the code and the UI makes it easy to port it to other platforms, the ViewModel became testable and you can test all logic in it, without bothering with the UI. Nice, no ?

The full source code for the project is at https://github.com/bsonnino/MvvmApp

 

There are times when you need to convert data from one format to the other and you don’t have any tools to do it, or you must do it so many times that it becomes difficult to do it manually. In this case, writing a C# program may be the easiest thing to do.

The program can’t be very difficult to code, at at some point it will be thrown, the procedure will be disposable. So, we’ll devise a simple way to convert the file (or files).

Let’s say we have a file like this one (obtained here):

<?xml version="1.0"?>
<catalog>
   <book id="bk101">
      <author>Gambardella, Matthew</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications 
      with XML.</description>
   </book>
   <book id="bk102">
      <author>Ralls, Kim</author>
      <title>Midnight Rain</title>
      <genre>Fantasy</genre>
      <price>5.95</price>
      <publish_date>2000-12-16</publish_date>
      <description>A former architect battles corporate zombies, 
      an evil sorceress, and her own childhood to become queen 
      of the world.</description>
   </book>
   <book id="bk103">
      <author>Corets, Eva</author>
      <title>Maeve Ascendant</title>
      <genre>Fantasy</genre>
      <price>5.95</price>
      <publish_date>2000-11-17</publish_date>
      <description>After the collapse of a nanotechnology 
      society in England, the young survivors lay the 
      foundation for a new society.</description>
   </book>
   <book id="bk104">
      <author>Corets, Eva</author>
      <title>Oberon's Legacy</title>
      <genre>Fantasy</genre>
      <price>5.95</price>
      <publish_date>2001-03-10</publish_date>
      <description>In post-apocalypse England, the mysterious 
      agent known only as Oberon helps to create a new life 
      for the inhabitants of London. Sequel to Maeve 
      Ascendant.</description>
   </book>
   <book id="bk105">
      <author>Corets, Eva</author>
      <title>The Sundered Grail</title>
      <genre>Fantasy</genre>
      <price>5.95</price>
      <publish_date>2001-09-10</publish_date>
      <description>The two daughters of Maeve, half-sisters, 
      battle one another for control of England. Sequel to 
      Oberon's Legacy.</description>
   </book>
   <book id="bk106">
      <author>Randall, Cynthia</author>
      <title>Lover Birds</title>
      <genre>Romance</genre>
      <price>4.95</price>
      <publish_date>2000-09-02</publish_date>
      <description>When Carla meets Paul at an ornithology 
      conference, tempers fly as feathers get ruffled.</description>
   </book>
   <book id="bk107">
      <author>Thurman, Paula</author>
      <title>Splish Splash</title>
      <genre>Romance</genre>
      <price>4.95</price>
      <publish_date>2000-11-02</publish_date>
      <description>A deep sea diver finds true love twenty 
      thousand leagues beneath the sea.</description>
   </book>
   <book id="bk108">
      <author>Knorr, Stefan</author>
      <title>Creepy Crawlies</title>
      <genre>Horror</genre>
      <price>4.95</price>
      <publish_date>2000-12-06</publish_date>
      <description>An anthology of horror stories about roaches,
      centipedes, scorpions  and other insects.</description>
   </book>
   <book id="bk109">
      <author>Kress, Peter</author>
      <title>Paradox Lost</title>
      <genre>Science Fiction</genre>
      <price>6.95</price>
      <publish_date>2000-11-02</publish_date>
      <description>After an inadvertant trip through a Heisenberg
      Uncertainty Device, James Salway discovers the problems 
      of being quantum.</description>
   </book>
   <book id="bk110">
      <author>O'Brien, Tim</author>
      <title>Microsoft .NET: The Programming Bible</title>
      <genre>Computer</genre>
      <price>36.95</price>
      <publish_date>2000-12-09</publish_date>
      <description>Microsoft's .NET initiative is explored in 
      detail in this deep programmer's reference.</description>
   </book>
   <book id="bk111">
      <author>O'Brien, Tim</author>
      <title>MSXML3: A Comprehensive Guide</title>
      <genre>Computer</genre>
      <price>36.95</price>
      <publish_date>2000-12-01</publish_date>
      <description>The Microsoft MSXML3 parser is covered in 
      detail, with attention to XML DOM interfaces, XSLT processing, 
      SAX and more.</description>
   </book>
   <book id="bk112">
      <author>Galos, Mike</author>
      <title>Visual Studio 7: A Comprehensive Guide</title>
      <genre>Computer</genre>
      <price>49.95</price>
      <publish_date>2001-04-16</publish_date>
      <description>Microsoft Visual Studio 7 is explored in depth,
      looking at how Visual Basic, Visual C++, C#, and ASP+ are 
      integrated into a comprehensive development 
      environment.</description>
   </book>
</catalog>

The first step would be convert the XML structure to a C# class. For this file, doing it manually can be an easy task, but for some files, it’s too much work. Thankfully, we have two easy ways to do it:

  • Using xsd.exe – xsd is a tool that converts xml to C# classes. All you have to do is to open a Visual Studio command prompt (in Visual Studio, go to Tools/Command Prompt) and type:
xsd books.xml
xsd /c books.xsd

The first command will create the Books.xsd file and the second one will create the Books.cs file that can be included in your project.

  • Using Visual Studio – now, Visual Studio has an easy way to convert XML and Json into C# (or VB.NET) classes. Just open the XML file, select the data and copy it to the clipboard. Then, in Visual Studio, create a new class, go to Edit/Paste Special/Paste XML as Classes and voilà- you have the C# class for the XML

With this class, we can read the XML file and deserialize it to an instance of the catalog class:

static void Main(string[] args)
{
    var serializer = new XmlSerializer(typeof(catalog));

    using var reader = new StreamReader("books.xml");
    var catalog = (catalog)serializer.Deserialize(reader);
    reader.Close();
}

Note that we are using the new C# 8 using statement feature to minimize nesting.

Now, we can use the new System.Text.Json namespace, available in .NET Core 3.1 or .NET 5.0, to convert the class to Json:

static void Main(string[] args)
{
    var serializer = new XmlSerializer(typeof(catalog));

    using var reader = new StreamReader("books.xml");
    var catalog = (catalog)serializer.Deserialize(reader);
    reader.Close();

    var options = new JsonSerializerOptions { WriteIndented = true };
    var jsonCatalog = JsonSerializer.Serialize(catalog, options);
    File.WriteAllText("books.json", jsonCatalog);
}

When we execute the code, we have the converter to convert the xml file into json. Yes, it works only with this kind of xml file, but it took us no time to assemble it. As a disposable program, it clearly does its job and you have the tool you need to convert the files.

The full source code for this project is at https://github.com/bsonnino/xmltojson

 

In the last post I showed how to use Xaml Islands to modernize your .NET app. As you could see, there are a lot of steps and the procedure is a little clumsy. But now it’s getting better, Microsoft has introduced WinUI 3 and, with that, things are getting better: you don’t need special components to add your WinUI code, you can use the WinUI components directly in you app. But there is a pitfall – right now, when I’m writing this article (end of 2020), Win UI 3 is still in preview and you need the preview version of Visual Studio to use it. You can install the preview version side-by-side with the production version, there is no problem with that. You can download the preview version from here. Once you download and install the preview version of Visual Studio, you need to download the WinUI3 templates to create new apps. This is done by installing the VSIX package from here. Now we are ready to modernize our app from the last post with WinUI3. In Visual Studio Preview, create a new app. We’ll create a new blank, packaged desktop app:\

clip_image002

When you create that app, Visual Studio will create a solution with two projects: a WPF app and a package app that will create a MSIX package that will allow your program to run in a sandbox and to send it to Windows Store:

clip_image003

If you run the application right now, the package will install the app, with a button in the center of the window, that will change its text when clicked. We’ll change the app to show our photos. The first step is to add the Newtonsoft.Json NuGet package. The MVVM Light package wasn’t ported to .NET 5.0, so we’ll use the MVVM framework from the Community toolkit (https://github.com/windows-toolkit/MVVM-Samples). In the NuGet package manager, install the Micosoft.Toolkit.MVVM package. Once you installed these packages, you can copy the Converters, Model, Photos and ViewModels paths from the WPFXamlIslands project from my Github. If you build the application, you will see some errors. Some of them are due to the conversion to WinUI3 and other ones are due to the use of the new MVVM Framework. Let’s begin converting the files for the framework. The MVVM Toolkit doesn’t have the ViewModelBase class, but has the similar ObservableObject. In MainViewModel.cs, changed the base class of the viewmodel to ObservableObject:

public class MainViewModel : ObservableObject

Then, we must change the IoC container that is used in MVVM Light to the one used in the MVVM toolkit. For that, you must add this code in App.xaml.cs:

/// <summary> 
/// Gets the current <see cref="App"/> instance in use 
/// </summary> 
public new static App Current => (App)Application.Current; 

/// <summary> 
/// Gets the <see cref="IServiceProvider"/> instance to resolve application services. 
/// </summary> 
public IServiceProvider Services { get; } 

/// <summary> 
/// Configures the services for the application. 
/// </summary> 
private static IServiceProvider ConfigureServices() 
{ 
  var services = new ServiceCollection(); 
  services.AddSingleton<MainViewModel>(); 
return services.BuildServiceProvider(); 
}

This code will configure the service collection, adding the instantiation of MainViewModel as a singleton, when required and will configure the service provider. We should call the ConfigureServices method in the constructor, like this:

public App() 
{
  Services = ConfigureServices();
  this.InitializeComponent();
  this.Suspending += OnSuspending; 
}

Now, we can get a reference to the ViewModel, when we need it, as the DataContext for the view. In MainWindow.xaml.cs, when you try to set the DataContext like this, you get an error, saying that DataContext is not defined:

public MainWindow()
{
    this.InitializeComponent();
    DataContext = App.Current.Services.GetService(typeof(MainViewModel));
}

That’s because the Window in WinUI isn’t a UIElement and thus, doesn’t have a DataContext. There are two workarounds to this issue:

  • Use x:Bind, that doesn’t need a DataContext for data binding
  • Set the DataContext to the main element of the window

So, we’ll choose the second option and initialize the DataContext with this code:

public MainWindow()
{
    this.InitializeComponent();
    MainGrid.DataContext = App.Current.Services.GetService(typeof(MainViewModel));
}

The next step is to remove the ViewModelLocator, as it won’t be needed. One other error is in the converter. The namespaces in the WinUI have changed, and we must change them to use the code. System.Windows.Data has changed to Microsoft.UI.Xaml.Data and System.Windows.Media.Imaging has changed to Microsoft.UI.Xaml.Media.Imaging (as you can see, there was a change from System.Windows to Microsoft.UI.Xaml).

Besides that, in WinUI, the interface IValueConverter is implemented, but the signatures of Convert and ConvertBack are different, so we have to change the signature of the methods (the code remains unchanged):

public object Convert(object value, Type targetType, object parameter, string language)
{
    string imagePath = $"{AppDomain.CurrentDomain.BaseDirectory}Photos\\{value}.jpg";
    BitmapImage bitmapImage = !string.IsNullOrWhiteSpace(value?.ToString()) &&
                              File.Exists(imagePath) ?
        new BitmapImage(new Uri(imagePath)) :
        null;
    return bitmapImage;
}

public object ConvertBack(object value, Type targetType, object parameter, string language)
{
    throw new NotImplementedException();
}

Now we can add the FlipView to the main window. In this case, we don’t need any component nor setup in the code behind – all the code is in MainWindow.xaml:

<Grid x:Name="MainGrid">
 <FlipView ItemsSource="{Binding Photos}">
   <FlipView.ItemTemplate>
     <DataTemplate>
       <Grid Margin="5">
         <Grid.RowDefinitions>
           <RowDefinition Height="*" />
           <RowDefinition Height="40" />
         </Grid.RowDefinitions>
         <Image Source="{Binding PhotoUrl}" Grid.Row="0" Margin="5"
             Stretch="Uniform" />
         <TextBlock Text="{Binding UserName}" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="1"/>
       </Grid>
     </DataTemplate>
   </FlipView.ItemTemplate>
 </FlipView>
</Grid>

With that, we can run the program and we get another error:

clip_image005

We get a DirectoryNotFoundException because of the package. When we are using the package, the current directory has changed and it’s not the install folder anymore, so we have to point to the full path. This is done with a code like this:

public MainViewModel()
{
    var fileName = $"{AppDomain.CurrentDomain.BaseDirectory}Photos\\__credits.json";
    Photos = JsonConvert.DeserializeObject&lt;Dictionary&lt;string, PhotoData&gt;&gt;(
        File.ReadAllText(fileName),
        new JsonSerializerSettings
        {
            ContractResolver = new DefaultContractResolver
            {
                NamingStrategy = new SnakeCaseNamingStrategy()
            }
        }).Select(p =&gt; new PhotoData() { PhotoUrl = $".\\Photos\\{p.Key}.jpg", UserName = p.Value.UserName });
}

Now, when you run the app, it should run ok, but I got a C++ exception:

clip_image007

In this case, I just unchecked the “Break when this exception is thrown” box and everything worked fine:

clip_image009

We now have our app modernized with WinUI3 components, there is no need to add a new component to interface, like XamlIslands and everything works as a single program. There were some things to update, like components that weren’t ported to .NET 5, new namespaces and some issues with the path, but the changes were pretty seamless. WinUI3 is still in preview and there will surely be changes before it goes to production, but you can have a glance of what is coming and how you can modernize your apps with WinUI 3.

All the code for this article is at https://github.com/bsonnino/WinUiViewer

You have an app developed a long time ago and it’s showing its age. It’s time to modernize it, but rewrite isn’t an option: it’s too complicated to rewrite it and it’s still working fine, there is no budget for rewriting the app, there are other priorities, and so on.

If the main reason for not rewriting the app is that it’s working fine, and the only thing that is showing its age is the UI, you have no reason to not modernize it. Microsoft sent the developers a clear message that WPF, Winforms and Win32 are alive and well, open sourcing them and porting to .NET Core. And, the best thing is that you can use the newest features in the Operating System and integrate them to your app without the need to rewrite it. You can even use the new UI of UWP apps in your own app, by using the technology named XamlIslands, where you can embed your own controls in your old app.

To show how this is done, we’ll create a WPF app that shows images and modernize it with Xaml Islands. For this app, I’ve dowloaded 30 images from http://unsample.net/ . This service sends me a zip file with a maximum of 30 photos, downloaded from https://unsplash.com, with a Json file with the credits. Our apps will show the photos and the credits.

Initially, go to http://unsample.net/ and download a set of 30 photos. In Visual Studio, create a new WPF app and name it WPFXamlIslands. In the Solution Explorer, create a new folder named Photos in the project and add the photos and the Json file from the zip to it. Select all files in the folder and change the Build Action to None and the Copy to Output Directory to Copy if Newer.

As we will be manipulating Json files, right-click on the References node and select Manage NuGet Packages, then install the Newtonsoft.Json package. After that, install the MVVM Light package, as we will be using MVVM for this project. You will have to remove the Microsoft.Practices.ServiceLocation using clause in the ViewModelLocator class and add the CommonServiceLocator using clause in the same file to make it compile.

Then, in MainWindow.xaml file, add this code:

<Window x:Class="WPFXamlIslands.MainWindow"
        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"
        xmlns:c="clr-namespace:WPFXamlIslands.Converters"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        DataContext="{Binding Source={StaticResource Locator}, Path=Main}">
    <Window.Resources>
        <c:StringToImageConverter x:Key="StringToImageConverter" />
    </Window.Resources>
    <Grid>
        <ScrollViewer HorizontalScrollBarVisibility="Disabled">
            <ItemsControl ItemsSource="{Binding Photos}" >
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Border BorderBrush="Black" Background="Beige"  BorderThickness="1" Margin="5">
                            <StackPanel Margin="5">
                                <Image Source="{Binding Key, Converter={StaticResource StringToImageConverter}}" 
                                       Width="150" Height="150" Stretch="Uniform" />
                                <TextBlock Text="{Binding Value.UserName}" MaxWidth="150" Margin="0,5" />
                            </StackPanel>
                        </Border>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</Window>

We’ve added a ItemsControl with a datatemplate to show the images and the name of the author. The items are presented in a WrapGrid, so the items are wrapped and the number of items change depending on the window width. To present the images, I’ve created a converter to convert the name of the image to a bitmap that can be assigned to the image:

public class StringToImageConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string imagePath = $"{AppDomain.CurrentDomain.BaseDirectory}Photos\\{value}.jpg";
        BitmapImage bitmapImage = !string.IsNullOrWhiteSpace(value?.ToString()) &&
            File.Exists(imagePath) ?
            new BitmapImage(new Uri(imagePath)) :
            null;
        return bitmapImage;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

It will take the name of the image and create a BitmapImage with it. That way, we can use the converter in the data binding for the list items. The MainViewModel will be like this:

public class MainViewModel : ViewModelBase
{
    /// <summary>
    /// Initializes a new instance of the MainViewModel class.
    /// </summary>
    public MainViewModel()
    {
        Photos = JsonConvert.DeserializeObject<Dictionary<string, PhotoData>>(
            File.ReadAllText("Photos\\__credits.json"),
            new JsonSerializerSettings
            {
                ContractResolver = new DefaultContractResolver
                {
                    NamingStrategy = new SnakeCaseNamingStrategy()
                }
            });
    }

    public Dictionary<string, PhotoData> Photos { get; private set; }

It will read the files, deserialize the Json file and assign the resulting Dictionary to the property Photos. This dictionary has the name of the file as the key and a class named PhotoData as the value. PhotoData is declared as:

public class PhotoData
{
    public string UserName { get; set; }
    public string UserUrl { get; set; }
    public string PhotoUrl { get; set; }
}

Now, when you run the application, it will show something like this:

The app runs fine, but it can be improved to add the new features and animations given by UWP, using the Xaml Islands.

The easiest way to use a UWP control in a WPF or Winforms app is to use the Windows Community Toolkit. This is a toolkit of components created by the community and Microsoft and can be found on https://github.com/windows-toolkit/WindowsCommunityToolkit.

To use a UWP control, you must use the WindowsXamlHost  control in the window. It can be found in the Microsoft.Toolkit.WPF.UI.XamlHost  NuGet package. Install it and add a WindowsXamlHost control in the main window:

<Grid>
    <xaml:WindowsXamlHost x:Name="UwpButton" 
                          InitialTypeName="Windows.UI.Xaml.Controls.Button"
                          ChildChanged="UwpButton_ChildChanged" />
</Grid>

In the code behind, you must add the code to initialize the button in the event handler for ChildChanged:

private void UwpButton_ChildChanged(object sender, EventArgs e)
{
    WindowsXamlHost windowsXamlHost = (WindowsXamlHost)sender;

    Windows.UI.Xaml.Controls.Button button =
        (Windows.UI.Xaml.Controls.Button)windowsXamlHost.Child;
    if (button == null)
        return;
    button.Width = 100;
    button.Height = 40;
    button.Content = "UWP button";
    button.Click += Button_Click;
}

private void Button_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
    MessageBox.Show("UWP button works");
}

The ChildChanged is called when the child in the XamlHost changes. There you must configure the control added as a child (with the use of the InitialTypeName property).

That should be everything, but when you see the code, you see that the Button is not defined. In the error window, there is a warning saying that that Windows.Foundation.UniversalApiContract is missing. My first try was to find a dll with this name, which couldn’t be found. Then I noticed that what was needed was not the dll, but a winmd file with the Windows Metadata for the controls. In fact, there is a Windows.Foundation.UniversalApiContract.winmd file located in C:\Program Files (x86)\Windows Kits\10\References\10.0.18362.0\Windows.Foundation.UniversalApiContract\8.0.0.0\ (the version in your system might change), and I added this file as a reference and the errors regarding the button disappeared.

Then I ran the project and got a Catatstrophic Failure (this one is nice – I was expecting my computer to melt down, but fortunately, that didn’t occur :-)). After some more research, I came to this article (yes, Microsoft also suffers with Catastrophic Failures :-)), and the answer was pretty simple: add an Application Manifest. In the Solution Explorer, add an Application Manifest and change it like this:

<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
  <application>
    <!-- A list of the Windows versions that this application has been tested on
         and is designed to work with. Uncomment the appropriate elements
         and Windows will automatically select the most compatible environment. -->

    <!-- Windows Vista -->
    <!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->

    <!-- Windows 7 -->
    <!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->

    <!-- Windows 8 -->
    <!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->

    <!-- Windows 8.1 -->
    <!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->

    <!-- Windows 10 -->
    <maxversiontested Id="10.0.18358.0"/>0
    <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />

  </application>
</compatibility>

That will set the MaxVersionTested and the error disappears. When you run the application, you will have something like this:

Now we can see that our program works with the UWP control, then let’s continue to modernize it. We will add a FlipView to show the images. For that, we must change the InitialTypeName of the WindowsXamlHost to show the FlipView:

<xaml:WindowsXamlHost x:Name="XamlHost" 
                      InitialTypeName="Windows.UI.Xaml.Controls.FlipView" 
                      ChildChanged="XamlHost_ChildChanged" />

The code for the ChildChanged event will configure the FlipView and its DataTemplate:

        private void XamlHost_ChildChanged(object sender, EventArgs e)
        {
            WindowsXamlHost windowsXamlHost = (WindowsXamlHost)sender;

            Windows.UI.Xaml.Controls.FlipView flipView =
                (Windows.UI.Xaml.Controls.FlipView)windowsXamlHost.Child;
            if (flipView == null)
                return;
            var dataTemplate = (Windows.UI.Xaml.DataTemplate)XamlReader.Load(@"
<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">
  <Grid Margin=""5"">
      <Grid.RowDefinitions>
         <RowDefinition Height=""*"" />
         <RowDefinition Height=""40"" />
      </Grid.RowDefinitions>
      <Image Source=""{Binding PhotoUrl}"" Grid.Row=""0"" Margin=""5""
            Stretch=""Uniform"" />
      <TextBlock Text=""{Binding UserName}"" HorizontalAlignment=""Center""
            VerticalAlignment=""Center"" Grid.Row=""1""/>
  </Grid>
</DataTemplate>");
            
            flipView.ItemTemplate = dataTemplate;
            flipView.ItemsSource = ((MainViewModel)DataContext).Photos;
        }

We create the DataTemplate as a string and load it with XamlReader.Read, then set the ItemsSource to the Photos property of the ViewModel. In order to use it in a UWP control, we modified the obtention of the Photos property:

public class MainViewModel : ViewModelBase
{
    /// <summary>
    /// Initializes a new instance of the MainViewModel class.
    /// </summary>
    public MainViewModel()
    {
        Photos = JsonConvert.DeserializeObject<Dictionary<string, PhotoData>>(
            File.ReadAllText("Photos\\__credits.json"),
            new JsonSerializerSettings
            {
                ContractResolver = new DefaultContractResolver
                {
                    NamingStrategy = new SnakeCaseNamingStrategy()
                }
            }).Select(p => new PhotoData() {PhotoUrl = $".\\Photos\\{p.Key}.jpg", UserName = p.Value.UserName});
    }

    public IEnumerable<PhotoData> Photos { get; private set; }

}

With these changes, you can now run the program and see the photos in a FlipView:

Conclusion

As you can see, you can modernize your desktop application with UWP controls, using Xaml Islands. The WindowsXamlHost eases this work a lot, but the work is still clumsy: you must add the winmd file, add the manifest to the project and manipulate the UWP control in code, using the Windows.UI.Xaml namespace. Adding a DataTemplate to the FlipView requires parsing Xaml code that comes from a string. Not a simple task, but still feasible. Hopefully, things will be easier with Project Reunion and WinUI 3.

All the source code for this article is available at https://github.com/bsonnino/WPFXamlIslands

After I finished last article, I started to think that there should be another way to test the non-virtual methods of a class. And, as a matter of fact, there is another way that has been around for a long time: Microsoft Fakes. If your don’t know it, you can read this article.

While I wouldn’t recommend it for using in your daily testing, it’s invaluable when you are testing legacy code and don’t have any testing and lots of coupling in your code. The usage is very simple:

In the Solution Explorer of the test project, right-click in the dll you want to create a fake (in our case, is the dll of the main project and select Add Fakes Assembly:

That will add the fake assembly and all the infrastructure needed to use fakes in your tests. Then, in your test class, you can use the newly created fake. Just create a ShimContext and use it while testing the class:

[TestMethod]
public void TestVirtualMethod()
{
    using (ShimsContext.Create())
    {
        var fakeClass = new Fakes.ShimClassNonVirtualMethods();
        var sut = new ClassToTest();
        sut.CallNonVirtualMethod(fakeClass);
    }
}

[TestMethod]
public void TestNonVirtualMethod()
{
    using (ShimsContext.Create())
    {
        var fakeClass = new Fakes.ShimClassNonVirtualMethods();
        var sut = new ClassToTest();
        sut.CallNonVirtualMethod(fakeClass);
    }
}

As you can see, we initialize a ShimsContext and enclose it in an Using clause. Then we initialize the fake class. This class will be in the same namespace as the original one, with .Fakes at the end, and its name will be the same, starting with Shim. That way, we can use it as a fake for the original class and it will have all non-virtual methods overridden.

That feature, although not recommended in a daily basis, can be a lifesaver when you have to test legacy code with deep coupling with databases, UI, or even other libraries. Microsoft Fakes can fake almost everything, including System calls, so it can be a nice starting point to refactor your legacy code – with it, you can put some tests in place and star refactoring your code more confidently.