Localizar Enumerações

July 5th, 2010

Continuando no desafio de localizar a minha aplicação, estou agora a atacar as enumerações. Esta também é uma óptima oportunidade de efectuar algum refactoring às classes e estruturas existentes.

Parte das enumerações usadas são automaticamente escritas com uma estrutura que era óptima.. até agora. A aplicação é N-layer, dividido em camadas lógicas. Por enquanto, e porque ainda não foi necessário mais, mantém-se monotier. As camadas lógicas neste caso são UI, BLL (Business Lógic Layer) e DAL (Data Access Layer), mais os repositórios de dados. Comum a todas as camadas há duas bibliotecas – MAAPPFramework que é um conjuntos de classes base, interfaces base, e muitos helpers comuns a várias aplicações (O MAAPP significa Miguel Alho Application : D ) e a camada de BO (Business Objects) que inclui as classes conceptuais e DTOs (Data Transfer Objects) do domínio. Os objectos nesta biblioteca são “dummy objects”, no sentido que não tem funcionalidade (senão algumas ordenações e extensões). Apenas suportam dados e estão devidamente decoradas para serem serializadas. Esta biblioteca é transformada em dll e utilizada por todas as camadas. Esta arquitectura está muito bem exemplificado pelo Imar Spaanjars nos seus artigos Building Layered Web Applications with Microsoft ASP.NET 2.0.

As enumerações que eu tinha incluíam sempre um valor numérico definido explicitamente (evitando súbitas alterações de referências para com a base de dados numa nova geração de código) e uma descrição com texto legível, em vez de um texto colado em CamelCase. um exemplo:

namespace Namespace.BO
{
    [Serializable]  
    public enum EnumTipoContacto
    {	
        [Description("Telefone")]
	Telefone = 1,
        [Description("Telemóvel")]
        Telemovel = 2,
        [Description("Email")]
        Email = 3,
        [Description("Fax")]
        Fax = 4,
        [Description("Outro Tipo de Contacto")]
        Outro = 5
    }
}

A descrição escrita como atributo, recorrendo ao System.ComponentModel.DescriptionAttribute permite em runtime ter uma versão legível do nome da enumeração, óptima para apresentação na UI, em vez do tipo valor devolvido pelo método ToString(). No exemplo em cima, O caso do “Outro” demonstra a possível diferença. Para obter os valores, na minha framework tenho um método numa helper class que permite obter o valor da descrição, bem como uma extension method para ajudar:

public class EnumDescription
{
        public static string GetDescription(Enum value)
        {
            FieldInfo fieldInfo = value.GetType().GetField(value.ToString());
            DescriptionAttribute[] attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
            return (attributes.Length > 0) ? attributes[0].Description : value.ToString();
        }
 
        //(...)
}
 
public static class EnumExtension
{
    public static string GetDescription(this Enum e)
    {
         return EnumDescription.GetDescription(e);
    }
}

Considerando que o código é gerado a partir de um modelo que controlo, é uma óptima solução, desde que localizado a uma definição linguística. Seria possível criar múltiplos atributos (costumizados) para suportar descrições localizadas, por exemplo, mas a manutenção e separação não me parece optimizado. Procurei outra solução que me permitiria utilizar ficheiros de recursos (.resx).

Uma das técnicas que encontrei parece simples e óptima. A descrição passa a ser definida no ficheiro de recursos, e a chave é o nome qualificado da enumeração. Optei pelo nome completo porque é me fácil de gerar com T4. Assim, a minha enumeração fica simplificada:

[Serializable]
public enum EnumTipoContacto
{	
	Telefone = 1,
        Telemovel = 2,
        Email = 3,
        Fax = 4,
        Outro = 5,
}

e o meu ficheiro .resx terá um conjunto de chaves valores qualificados, que pode ser localizado na versão do ficheiro para outra língua:

(...)
<data name="Namespace.BO.EnumTipoContacto.Telefone" xml:space="preserve">
    <value>Telefone</value>
</data>  
 <data name="Namespace.BO.EnumTipoContacto.Telemovel" xml:space="preserve">
    <value>Telemóvel</value>
</data>  
 <data name="Namespace.BO.EnumTipoContacto.Email" xml:space="preserve">
    <value>Email</value>
</data>  
 <data name="Namespace.BO.EnumTipoContacto.Fax" xml:space="preserve">
    <value>Fax</value>
</data>  
 <data name="Namespace.BO.EnumTipoContacto.Outro" xml:space="preserve">
    <value>Outro Tipo / Desconhecido</value>
</data> 
(...)

Para obter o valor da chave, a operação é ligeiramente diferente:

public static string GetLocalizedDescription(EnumTipoContacto tipo)
{
      ResourceManager resources = Namespace.BO.Properties.Enum.ResourceManager;
 
      string resourceKey = String.Format("{0}.{1}", tipo.GetType(), tipo);
      string localizedDescription = resources.GetString(resourceKey);
 
      if (localizedDescription.IsNullOrEmpty())
           return tipo.ToString();
      else
           return localizedDescription;
}

Neste exemplo, tipo.GetType() devolve Namespace.BO.EnumTipoContacto, permitindo construir a chave. é necessário ter em atenção que linha

ResourceManager resources = Namespace.BO.Properties.Enum.ResourceManager;

obriga-me a que a assemblagem gerada seja o Namespace.BO.dll e o ficheiro de recursos é o Enum.resx ou Enum.xx.resx onde xx representa o código cultural (“es”, por exemplo). Novamente, porque o meu código é em boa parte autogerado, tenho controlo sobre esse formato.

Outro método muito útil, para usar como DataSource para preencher DropDownLists e controlos do género é a seguinte:

public static List<KeyValuePair<string, string>> GetDDLList()
{
            List<KeyValuePair<string, string>> kvPairList = new List<KeyValuePair<string, string>>();
            ResourceManager resources = Namespace.BO.Properties.Enum.ResourceManager;
 
            foreach (Enum enumValue in Enum.GetValues(typeof(EnumTipoContacto)))
            {
                string resourceKey = String.Format("{0}.{1}", enumValue.GetType(), enumValue);
                string localizedDescription = resources.GetString(resourceKey);
 
                if(localizedDescription.IsNullOrEmpty())
                    kvPairList.Add(new KeyValuePair<string, string>(enumValue.ToString(), enumValue.ToString()));
                else
                    kvPairList.Add(new KeyValuePair<string, string>(enumValue.ToString(), localizedDescription));
            }
 
            return kvPairList;
}

Semelhante à anterior, permite obter uma lista de KeyValuePairs aceites como DataSource dos controlos.

Este foi, numa fase inicial, o tipo de estrutura que mais me preocupava, no sentido que os atributos poderiam complicar a globalização, e obrigar a um refactoring interno profundo. Felizmente, um simples workaround e refactoring evitou grandes mexidas. Também, porque nem os nomes dos métodos (nomeadamente o GetDDLList() ) nem os tipos de dados foram alterados, a camada superior continua a funcionar sem ter sido afectado.

Override de ToString() e operadores novos

January 15th, 2009

Ontem estive numa curta discussão acerca das potencialidades de herança nas classes, em programação por objectos, em especial no .NET, com um amigo que está a estagiar comigo. Bastou um pequeno exemplo para demonstrar alguns dos princípios.

Comecei por recordar o principio da herança com um diagrama muito simples:

Neste exemplo ClasseBase, como o nome sugere, é base de ClasseDerivado1 e ClasseDerivado2, e tendo propriedades publicas e métodos publicos, essas propriedades estarão, tambem, presentes em nas classes derivadas. Ou seja

ClasseDerivado1 tem (publicamente) acessível as propriedades prop1, prop2 e prop3, e ainda os metódos meth1 e meth2.
ClasseDerivado2 tem (publicamente) acessível as propriedades prop1, prop2 e prop4, e ainda os metódos meth1 e meth3.

A conversa depois seguiu para o facto de poder fazer overide de métodos da base na classe derivada. Em concreto falamos do ToString(). Todos os objectos criados em .NET derivado da base Object, que, entre outras, tem o método ToString(). Por defeito este devolve o valor da instancia (quando é um tipo de valor, por exemplo, int, byte, bool, char..). Quando se trata de um tipo que é referência (exceptuando a string), é devolvido o nome completo da classe (namespaceCompleto.nomeDaClasse). No entanto, nada me impede de redefinir o ToString(). Por exemplo, numa classe “Pessoa”, posso querer que o ToString() devolva a concatenação do primeiro e último nome.

Como conseguir?

class QQcoisa
    {
        private string _myVar = "olá";
        [XmlAttribute]
        public string myVar
        {
            get { return _myVar; }
            set { _myVar = value; }
        }
 
        public override string ToString()
        {
            return base.ToString() + " " + myVar;
        }
 
        public static string operator +(QQcoisa in1, QQcoisa in2)
        {
            return in1.myVar + in2.myVar;
        }
    }

Usando a declaração “public overide string ToString()”, posso redefinir o funcionamento do método. Se necessitar de recorrer, por alguma razão, ao ToString() da base, posso através da instrução

base.ToString();

No exemplo em cima, o método devolve o nome da class (“QQcoisa”) concatenado com um espaço e por fim o valor em myVar (que por defeito é “olá”).

É um exemplo simples do poder e flexibilidade de OOP.

A conversa continuou com a menção da criação de operadores para as classes. As potencialidades criadas pro esta funcionalidade é enorme. Por exemplo posso no meu código definir o que é uma soma de dois objectos (que não são valores). Por exemplo posso definir que somar dois QQcoisa é somar os valores presentes em myVar de cada um das instâncias.

Nem todos os operadores permitem redefinição. A lista dos operadores que podem ser redefinidos estão no MSDN. Para redefinir, usa-se a declaração:

public static string operator +(QQcoisa in1, QQcoisa in2)

Simples, até.