sexta-feira, 17 de julho de 2015

Custom ShowModalDialog with JQuery!

Já é de conhecimento de todos (ou da maioria) que a API [window.showModalDialog] foi descontinuada pelo Browser Chrome, a partir da versão 37 - Disabling showModalDialog.

Com isto muitos WebSites simplesmente não executam mais este recurso neste browser.

Como ele é usado extensamente, o que fazer então em substituição ao depreciado showModalDialog? Vamos criar um próprio, usando a tecnologia JQuery!

A função ShowModalDialog


if (typeof (GT) == "undefined") { GT = {}; }

GT.Functions = {
    ShowModalDialog: function (content, parent, width, height, bkgImage, callbackFn) {
        var divTag = '<div id="divDialog" ';
        divTag += 'style="border: 6px solid gray; overflow:hidden; box-shadow: 1px 1px 5px #333; ">';
 
        if (bkgImage) {
            divTag += '<div id="divDialogLoading" ';
            divTag += 'style="width:' + width + 'px; height:' + height + 'px; ';
            divTag += 'background: white url(' + bkgImage + ') no-repeat center;" >';
            divTag += '</div>';
        }
 
        divTag += '<iframe id="frameDialog" frameBorder="0" scrolling="no" ';
        divTag += 'src="' + content + '" ';
        divTag += 'style="width:100%;" ';
 
        divTag += '>';
        divTag += '</iframe>';
 
        divTag += '</div>';
 
        var div = null;
        var $JQ = null;
 
        if (parent != null) {
            var $JQ = parent.jQuery.noConflict();
            parent.window.defer = $JQ.Deferred();
            div = $JQ(divTag).appendTo("body");
        }
        else {
            $JQ = jQuery.noConflict();
            window.defer = $JQ.Deferred();
            div = $JQ(divTag);
        }
 
        div.append('<style type="text/css"> \
        .ui-widget-overlay { \
            position: absolute; \
            top: 0; \
            left: 0; \
            width: 100%; \
            height: 100%; \
            background: #aaaaaa;\
            opacity: 0.3; \
            } \
        .ui-front { \
            z-index: 100; \
            } \
        </style>');
 
        div.dialog({
            position: {
                my: "center",
                at: "center",
                of: div,
                within: div
            },            
            autoOpen: false,
            closeOnEscape: false,
            height: height,
            width: width,
            modal: true,
            open: function () {
                $JQ('#frameDialog').on("load", function () {
                    $JQ("#divDialogLoading").hide();
                    this.height = div.height();
                });
            },
            close: function () {
                if (callbackFn) callbackFn();
            }
        });
 
        div.dialog('open');
 
        return (parent != null ? parent.window.defer.promise() : window.defer.promise());
 
    }
};


Algumas considerações

Em si a função é simples pois utiliza o componente JQuery UI Dialog para mostrar um diálogo para o usuário. O mais importante é o uso do método [$.Deferred()] e o retorno da função, que utiliza o método [promise].

É justamente isto que garante que o diálogo se comporte como a API window.showModalDialog, trabalhando de forma síncrona com o resto do seu código JavaScript.

Importante: Se você pretende usar as versões mais recentes (a partir da 1.7.x) do JQuery UI Dialog em uma página ASPX, não se esqueça de usar a seguinte declaração no documento de sua página

<!DOCTYPE html>

Isto garante que o diálogo vai ser posicionado corretamente (no caso da nossa função, no centro da página).

Como devo usar o método

Veja abaixo dois exemplos de uso do método. O primeiro uso é básico. O segundo é interessante porque permite ao desenvolvedor executar o método a partir de uma página dentro de um [iframe].


var url = "http://MeuSite/MinhaPagina.aspx?param=test";

GT.Functions.ShowModalDialog(
 url,
 null,
 470,
 470,
 null,
 function result() {
  window.defer.then(
   function (result) {
    if (result)
    {
     // TRABALHA COM O RETORNO DA PÁGINA (url) PASSADA POR PARÂMETRO
    }
   });
 });



var url = "http://MeuSite/MinhaPagina.aspx?param=test";
var _parentW = (document.parentWindow ? document.parentWindow.parent : document.defaultView.parent);
var $jParent = _parentW.jQuery.noConflict();

GT.Functions.ShowModalDialog(
 url,
 _parentW,
 470,
 470,
 null,
 function result() {
  _parentW.window.defer.then(
   function (result) {
    if (result)
    {
     // TRABALHA COM O RETORNO DA PÁGINA (url) PASSADA POR PARÂMETRO
    }
   });
 });


E por fim precisamos retornar um valor para o método através da nossa página de exemplo (MinhaPagina.aspx). Note novamente que temos um exemplo de retorno simples e outro na qual a página ASPX se encontra dentro de um [iframe]


function CloseDialog()
{
 window.defer.resolve("VALOR PARA RETORNAR");
 $("#divDialog").dialog("close");
 $("#divDialog").dialog("destroy");
 $("#divDialog").remove();
}



function CloseDialog()
{
 var _parentW = (document.parentWindow ? document.parentWindow.parent : document.defaultView.parent);
 _parentW.window.defer.resolve("VALOR PARA RETORNAR");
 var $jParent = _parentW.jQuery.noConflict();
 $jParent("#divDialog").dialog("close");
 $jParent("#divDialog").dialog("destroy");
 $jParent("#divDialog").remove();
}


Refs:
JQuery UI Dialog
JQuery Deferred API

Cheers!!

quinta-feira, 23 de abril de 2015

CRM Tips - Import Solution - RibbonCustomization

Por um acaso, ao tentar importar uma solução no CRM, já se deparou com o seguinte erro:

There should only be at most one instance of RibbonCustomization per solution per entity. Current entity: [entityName]. Current solution: [guid]

Pois bem, muito provavelmente o metadados de Ribbons do seu CRM está, digamos, corrompido! São vários os motivos para que isto possa ter ocorrido, como por exemplo, em uma migração de versão.

Como se corrige isto, então? Atue no metadados de Ribbons, eliminando as duplicidades.

A Procedure abaixo faz justamente isto, para todas as entidades do banco do CRM.


declare @tables table (id int identity, name nvarchar(100))

declare @count int, @currentTable nvarchar(100), @sql nvarchar(max)

declare @RibbonCustomizationId uniqueidentifier, 
  @RibbonCustomizationUniqueId uniqueidentifier,
  @Entity nvarchar(100),
  @SolutionId uniqueidentifier,
  @SupportingSolutionId uniqueidentifier,
  @ComponentState int,
  @OverwriteTime datetime,
  @OrganizationId uniqueidentifier,
  @PublishedOn datetime,
  @IsManaged bit

-- BUSCA TODAS AS TABELAS DO CRM
insert into @tables (name)
select name
from sysobjects
where type = 'v'
order by name

-- LOOP NAS TABELAS
select @count = count(*) from @tables
while (@count > 0)
begin
 select @currentTable = name from @tables where id = @count

 -- CRIA UMA TEMP TABLE PARA GUARDAR OS RIBBONS AGRUPADOS POR [SolutionId]
 declare @Unique_RibbonCustomization table (
  RibbonCustomizationId uniqueidentifier, 
  RibbonCustomizationUniqueId uniqueidentifier,
  Entity nvarchar(100),
  SolutionId uniqueidentifier,
  SupportingSolutionId uniqueidentifier,
  ComponentState int,
  OverwriteTime datetime,
  OrganizationId uniqueidentifier,
  PublishedOn datetime,
  IsManaged bit)

 insert into @Unique_RibbonCustomization
  select RibbonCustomizationId, RibbonCustomizationUniqueId, Entity, SolutionId, SupportingSolutionId, ComponentState, OverwriteTime, OrganizationId, PublishedOn, IsManaged
  from (
     select *,
      row_number() over (partition by SolutionId order by SolutionId) as row_number
     from RibbonCustomization
     where entity = @currentTable
     ) as r
  where row_number = 1

 -- GUARDA OS IDS (RibbonCustomizationId) QUE POSSUEM DUPLICIDADE DE RIBBON
 declare @Diff_RibbonCustomization table (RibbonCustomizationId uniqueidentifier)
 insert into @Diff_RibbonCustomization
  select RibbonCustomizationId
  from RibbonCustomization
  where entity = @currentTable and
     RibbonCustomizationId not in (select RibbonCustomizationId from @Unique_RibbonCustomization)

 if (exists(select * from @Diff_RibbonCustomization))
 begin

  print 'Correcting EntityRibbon for: ' + @currentTable + '...'

  -- EXCLUIR OS RIBBONS DUPLICADOS PARA A ENTIDADE DO LOOP
  delete from RibbonCustomization 
  where Entity = @currentTable and
     RibbonCustomizationId in (select RibbonCustomizationId from @Diff_RibbonCustomization)

  -- EXCLUI AS DEPENDÊNCIAS (SolutionComponentBase, dependencybase e DependencyNodeBase)
   delete from SolutionComponentBase where ObjectId in (select RibbonCustomizationId from @Diff_RibbonCustomization)

  delete db from dependencybase db 
      inner join dependencynodebase dnb 
     on db.dependentcomponentnodeid = dnb.dependencynodeid or
     db.RequiredComponentNodeId = dnb.dependencynodeid
  where dnb.ObjectId in (select RibbonCustomizationId from @Diff_RibbonCustomization)
 
  delete from DependencyNodeBase where ObjectId in (select RibbonCustomizationId from @Diff_RibbonCustomization)
 end

 select @count = @count - 1, @currentTable = null
end

print 'Done.'


Após rodar a proc você conseguirá importar sua solução sem problemas - espero! :)

[]s

terça-feira, 21 de abril de 2015

CRM Tips - Managed to UnManaged Solution

Se por um acaso - ou falta de conhecimento - você criou uma solução Gerenciada no CRM por engano e precisa convertê-la para não gerenciada, segue um Script que faz este trabalho por você, já que até o momento a MS não disponibilizou uma opção para isto na ferramenta.

Importante notar que esta ação é sim suportada porque inclusive é ensinada no TechEd (aos 43:08 min do vídeo referenciado no final deste artigo). Eu somente fiz algumas alterações na procedure para atender melhor nosso cenário.

No script abaixo substitua a palavra [SolutionName] pelo nome de sua solução gerenciada que deseja converter.


declare @solutionId uniqueidentifier, @systemSolutionId uniqueidentifier
select @solutionId = solutionid from solutionbase where uniquename = 'SolutionName'
select @systemSolutionId = solutionid from solutionbase where uniquename = 'active'

update publisherbase 
set isreadonly = 0 
where publisherid in
(
select publisherid from solutionbase where solutionid = @solutionId
)

declare @tables table (id int identity, name nvarchar(100), ismanaged bit, issolution bit)
declare @count int, @currentTable nvarchar(100), @currentM bit, @currentS bit, @sql nvarchar(max)

insert into @tables (name, ismanaged, issolution)
select name, 1, 0
from sysobjects
where id in
(
select id from syscolumns where name in ('ismanaged')
)
and type = 'u'
order by name

insert into @tables (name, ismanaged, issolution)
select name, 0, 1
from sysobjects
where id in
(
select id from syscolumns where name in ('solutionid')
)
and type = 'u'
and name not in ('solutioncomponentbase')
order by name

select @count = count(*) from @tables

while (@count > 0)
begin
 select @currentTable = name, @currentM = ismanaged, @currentS = issolution from @tables where id = @count

 if (@currentM = 1)
 begin
  select @sql = 'update ' + @currentTable + ' set isManaged = 0 where SolutionId = N''' + cast(@solutionId as nvarchar(100)) + ''''
  exec (@sql)
 end

 print 'updated IsManaged to 0 on: ' + @currentTable

 select @count = @count - 1, @currentTable = null
end


Basicamente este código busca todas as tabelas que possuem os atributos [ismanaged] e [solutionid] para poder transformar os componentes da solução em não gerenciáveis e redirecionar as dependências para a solução interna do CRM chamada [Active].

Ref: Advanced Bag of Tips & Tricks for Microsoft Dynamics CRM 2011 Developers

[]s

sexta-feira, 19 de setembro de 2014

Excel + C# - Let's Play with it!

Programar no Office está cada vez melhor, graças as bibliotecas OpenXML. Vamos brincar um pouco com a criação e manipulação de um arquivo Excel.

Primeiro vamos baixar a biblioteca OpenXML (2.0) e adicioná-la no Visual Studio

Baixe o instalador Open XML SDK 2.0 for Microsoft Office. Baixe os dois arquivos [msi], sendo que usaremos no VS o arquivo OpenXMLSDKv2.msi .

Eu já expliquei um pouco o uso desta tecnologia aqui.

Vamos então criar um novo projeto no VS (CONSOLE Application) e adicionar a referência da biblioteca [DocumentFormat.OpenXml] como ilustrado na figura abaixo



Para criar o arquivo Excel, utilize a seguinte função especificada abaixo. Não esqueça antes as seguintes referências no código:


using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;



private static void CriarTemplate(string template)
{
 using (var doc = SpreadsheetDocument.Create(template, SpreadsheetDocumentType.Workbook))
 {
  doc.AddWorkbookPart().AddNewPart<WorksheetPart>().Worksheet = new Worksheet(new SheetData());

  WorkbookStylesPart stylesPartDef = doc.WorkbookPart.AddNewPart<WorkbookStylesPart>();
  stylesPartDef.Stylesheet = GenerateStyleSheet();
  stylesPartDef.Stylesheet.Save();

  doc.WorkbookPart.Workbook =
    new Workbook(
   new Sheets(
     new Sheet
     {
      Id = doc.WorkbookPart.GetIdOfPart(doc.WorkbookPart.WorksheetParts.First()),
      SheetId = 1,
      Name = "Sheet1"
     }));

  string relId = doc.WorkbookPart.Workbook.Descendants<Sheet>().First().Id;
  var wSheetPart = (WorksheetPart)doc.WorkbookPart.GetPartById(relId);

  //var mergeCells = new MergeCells();
  //wSheetPart.Worksheet.InsertAfter(mergeCells, wSheetPart.Worksheet.Elements<SheetData>().First());
 }
}


Faça a chamada do método CriarTemplate para criar o arquivo excel


static void Main(string[] args)
{
 string template = @"C:\TEMP\Test.xlsx";
 CriarTemplate(template);
}


Com isto já podemos manipular o template. Vamos abrí-lo e criar um conteúdo simples na célula [A1]


private static void ManipularTemplate(string template)
{
 using (SpreadsheetDocument documento = SpreadsheetDocument.Open(template, true))
 {
  var wBookPart = documento.WorkbookPart;
  string relId = wBookPart.Workbook.Descendants<Sheet>().First().Id;
  var wSheetPart = (WorksheetPart)wBookPart.GetPartById(relId);

  // CRIA A LINHA 1 COM MERGE DE COLUNAS (1 a 10)...
  var rowIndex = 1;
  var row = new Row() { RowIndex = UInt32.Parse(rowIndex.ToString()) };
  row.Append(CreateCell(1, rowIndex, 0, CellValues.String, "LINHA 1"));
  wSheetPart.Worksheet.First().AppendChild(row);

  //fnMergeCells(wSheetPart, rowIndex, 1, 10);

  wSheetPart.Worksheet.Save();
 }
}


Adicione as classes de apoio, para criação da célula, merge de células e conversão de número de coluna em letras.



private static Cell CreateCell(int colIndex, int rowIndex, int styleIndex, CellValues cellValues, string cellValueText)
{
 var cell = new Cell() { CellReference = getCellReference(colIndex) + rowIndex.ToString(), StyleIndex = (UInt32Value)(UInt32)styleIndex, DataType = cellValues };
 var cellValue = new CellValue();
 cellValue.Text = cellValueText;
 cell.Append(cellValue);
 return cell;
}

private static void fnMergeCells(WorksheetPart wSheetPart, int rowIndex, int de_Min, int ate_Max)
{
 var mergeCell = new MergeCell() { Reference = new StringValue(getCellReference(de_Min) + rowIndex.ToString() + ":" + getCellReference(ate_Max + 1) + rowIndex.ToString()) };

 var mergeCells = wSheetPart.Worksheet.Elements<MergeCells>().First();
 mergeCells.Append(mergeCell);

 wSheetPart.Worksheet.Save();
}

private static StringValue getCellReference(int colIndex)
{
 string columnString = "";
 decimal columnNumber = colIndex;
 while (columnNumber > 0)
 {
  decimal currentLetterNumber = (columnNumber - 1) % 26;
  char currentLetter = (char)(currentLetterNumber + 65);
  columnString = currentLetter + columnString;
  columnNumber = (columnNumber - (currentLetterNumber + 1)) / 26;
 }
 return columnString;
}

private static Stylesheet GenerateStyleSheet()
{
 var styleSheet = new Stylesheet();

 var fonts = new Fonts();
 var fontDef = new Font(
      new FontSize() { Val = 11 },
      new Color() { Rgb = new HexBinaryValue() { Value = "000000" } },
      new FontName() { Val = "Calibri" });

 fonts.Append(fontDef);

 var fills = new Fills(
     new Fill(
      new PatternFill() { PatternType = PatternValues.None }
     ),
     new Fill(
      new PatternFill() { PatternType = PatternValues.Gray125 }
     ),
  // BLUE COLOR
     new Fill(
      new PatternFill(
       new ForegroundColor() { Theme = (UInt32Value)4U, Tint = 0.39997558519241921D },
       new BackgroundColor() { Indexed = (UInt32Value)64U }
      ) { PatternType = PatternValues.Solid }
     )
    );

 var borders = new Borders(
      new Border(new LeftBorder(), new RightBorder(), new TopBorder(), new BottomBorder(), new DiagonalBorder())
      );

 // CellFormats IS THE CLASS USED IN THE PROPERTIES [StyleIndex] FOR ALL OBJECTS
 var cellFormats = new CellFormats(
  // StyleIndex[0] - Default
       new CellFormat() { FontId = 0, FillId = 0, BorderId = 0 },
  // StyleIndex[1] - FONT BLUE
       new CellFormat() { FontId = 0, FillId = 2, BorderId = 0, ApplyFill = true }
       );

 styleSheet.Append(fonts);
 styleSheet.Append(fills);
 styleSheet.Append(borders);
 styleSheet.Append(cellFormats);

 return styleSheet;
}


Efetue a chamada do método ManipularTemplate


static void Main(string[] args)
{
 string template = @"C:\TEMP\Test.xlsx";
 CriarTemplate(template);
 ManipularTemplate(template);
}


Não se esqueça de adicionar outra referência importante no projeto, a dll WindowsBase



Execute o projeto e terá como resultado (o projeto cria o arquivo em c:\temp)



Agora vamos trabalhar com merge e estilo de células. Vamos efetuar o merge das 10 primeiras células e pintá-la de azul. Primeiro, altere o método [Manipular Template] e altere a linha de


row.Append(CreateCell(1, rowIndex, 0, CellValues.String, "LINHA 1"));

PARA 


row.Append(CreateCell(1, rowIndex, 1, CellValues.String, "LINHA 1"));


Note que a diferença está no terceiro valor do parâmetro da função [CreateCell]. Ela indica qual StyleIndex usar. Para que serve o StyleIndex? Justamente para indicar qual Estilo aplicar na célula. Onde ficam estes estilos? Quando criamos o método [CriarTemplate] já especificamos os estilos na criação do arquivo, na linha


stylesPartDef.Stylesheet = GenerateStyleSheet();
stylesPartDef.Stylesheet.Save();


Analise o método GenerateStyleSheet. Nele criamos todos os estilos necessários como tipo de fonte, preenchimento e borda. Porém ainda precisamos reunir todos estes estilos para aplicar na StyleIndex das células. Por exemplo, eu preciso de um fonte Bold, da célula com preenchimento azul, da célula com borda, etc. Como fazer isto? Apenas use a classe [CellFormats]. Ela junta tudo para você, de forma que você pode criar quantas CellFormats forem necessárias. No nosso exemplo criamos os seguintes CellFormats


var cellFormats = new CellFormats(
 // StyleIndex[0] - Default
      new CellFormat() { FontId = 0, FillId = 0, BorderId = 0 },
 // StyleIndex[1] - FONT BLUE
      new CellFormat() { FontId = 0, FillId = 2, BorderId = 0, ApplyFill = true }
      );


Vamos aplicar agora o recurso de merge. Entre no método [ManipularTemplate] e habilite a seguinte linha comentada


fnMergeCells(wSheetPart, rowIndex, 1, 10);


Entre também no método [CriarTemplate] e habilite as linhas


var mergeCells = new MergeCells();
wSheetPart.Worksheet.InsertAfter(mergeCells, wSheetPart.Worksheet.Elements<SheetData>().First());


E o resultado agora



That's It!
[]s

quarta-feira, 27 de agosto de 2014

Reporting Services - Girar Imagem usando VB.NET

Um recurso interessante do reporting services é a possibilidade de execução de código VB.NET no seu relatório. Com isto em mãos podemos resolver inúmeras situações com pouco código. Imagine então a situação em que você possui uma imagem para ser adicionada no seu relatório. Porém o cliente precisa que esta imagem seja visualizada em um grau diferente, como por exemplo, na vertical. Não existe uma propriedade do componente de imagem na qual possa ser configurada esta alteração. Felizmente, via programação, resolvemos isto de forma bem simples, conforme mostrado abaixo.

Primeiro, vamos adicionar a referência da classe Drawing no relatório (opção Propriedades)



E vamos adicionar nosso código para girar a imagem




Function GirarImagem(ByVal imageDB As Byte()) As Byte()

        Dim ms As System.IO.Stream = New System.IO.MemoryStream(imageDB)
        ms.Seek(0, System.IO.SeekOrigin.Begin)

        Dim imgObj As System.Drawing.Bitmap = New System.Drawing.Bitmap(ms)
        imgObj.RotateFlip(System.Drawing.RotateFlipType.Rotate270FlipXY)

        Dim ms2 As System.IO.MemoryStream = New System.IO.MemoryStream()
        imgObj.Save(ms2, System.Drawing.Imaging.ImageFormat.Bmp)
        Dim bitmapData As Byte() = ms2.ToArray()

        Return bitmapData

End Function


Note que a função espera um parâmetro de imagem do tipo [Byte()], pois neste nosso exemplo o relatório busca a imagem como um campo no banco de dados (VarBinary)



Desta forma, vamos alterar a expressão do objeto de imagem para executar nossa função



E o resultado



Ref: Rotate Flip Types

[]s

terça-feira, 19 de agosto de 2014

JQuery AutoComplete - IE Performance

Um dos mais úteis componentes da lista do JQuery UI sem dúvida é o AutoComplete. Porém, existe um problema conhecido de performance ao usar este componente no [Internet Explorer] quando a lista de informações passa de 1000 registros.

Observando mais de perto este problema podemos perceber que no IE a [renderização] dos dados é a causa do problema e não a quantidade de informações da lista em si.

Desta forma, se você estiver passando por este problema no IE poderá alterar o atributo [source] do componente para executar uma função específica criada por você para justamente [limitar] a quantidade de dados de retorno que será renderizada no Browser.

Segue um exemplo:

No momento da transformação do seu TextBox em um Lookup, adicione a chamada da sua função (CustomAutoCompleteFn)


with ($("#TEXTBOXID")) {
        autocomplete({
            source: function (reg, responseFn) { CustomAutoCompleteFn(reg, responseFn) },
            delay: 0,
            minLength: 2
        });
}


E a função em si:


function CustomAutoCompleteFn(req, responseFn) {
    // LIMITANDO A LISTA DE RESULTADOS POR RETORNAR OS 40 PRIMEIROS
    var b = new Array();
    var re = $.ui.autocomplete.escapeRegex(req.term);
    var matcher = new RegExp("\\b" + re, "i");
    var a = $.grep(listInfo, function (item, index) {
        r = matcher.test(item.value);
        return r;
    });
    b = a.slice(0, 40);
    responseFn(b);
}


O interessante desta função é que você mesmo efetua o filtro no seu Array de informações (var listInfo) e retorna para ser renderizado no Browser apenas a quantidade que deseja, através da linha [b = a.slice(0, 40)].

Desta forma sua performance fica garantida mesmo com uma lista de informações extensa.

Fonte: JQuery Forum

[]