Blog Page

CRM 2011 – Nummerngenerierung (Laufnummer erzeugen)

Leider ein sehr leidiges Thema in der CRM-Entwicklung. Microsoft arbeitet mit GUIDs als Primärschlüssel, was einerseits natürlich sinnvoll ist, andererseits keinen „Schlüssel“ bzw. „Autowert“ zur Verfügung stellt, mit welchem man arbeiten kann. CRM bietet stattdessen die sog. „Automatische Nummerierung“ an. Leider hat diese Funktionalität einen riesen Haken: Die Funktionalität steht nur für die Entitäten Verträge, Anfragen, Angebote, Aufträge, Artikel, Rechnungen und Kampagnen zur Verfügung.

Bei einem größeren Projekt, z.B. wenn man komplett eigene Geschäftsprozesse, ist das natürlich ein wahnsinn. Keine Ahnung was Microsoft sich da hat einfallen lassen. Vermutlich kommt hier das übliche Argument zum tragen: „Für 95% der Kunden reicht’s aus. Wir wollen unseren Implementierungspartnern nicht die komplette Arbeit abnehmen.“. Naja, ich war noch nie ein großer Fan dieser Strategie…

Um das Problem zu lösen, habe ich eine eigene Entität erstellt, welche ich „Laufnummer“ genannt habe. Diese Entität hat keine Ansichten/Formulare, sie besitzt lediglich die Felder „neu_laufnummerid“ und „neu_nummer“.

Nachdem ich in meinen CRM-Projekten stark auf Webservices setze, habe ich eine Klasse zum Erzeugen der Laufnummer (nach Schema JJMMTT001) entwickelt, welche diese Nummer für den aktuellen Tag (in der Entität) sucht und ggf. hochzählt. Falls die Laufnummer (für diesen Tag) noch nicht generiert wurde, wird sie natürlich neu erzeugt:

[sourcecode language=“csharp“]
public class Laufnummer
{
public const string EntityName = "neu_laufnummer";
public const string EntityPrimaryKey = "neu_laufnummerid";

/// <summary>
/// Finds the highest verkaufsprojekt nummer.
/// </summary>
/// <param name="organization">The organization.</param>
/// <returns></returns>
public static string GetNew(contracts.IOrganization organization)
{
string laufNummer;
string suffix = StringHelper.FillLeftZero(1, 3);

QueryExpression query = new QueryExpression();
query.ColumnSet.AddColumns("neu_laufnummerid", "neu_nummer");
query.EntityName = EntityName;
query.AddOrder("neu_nummer", OrderType.Descending);
query.PageInfo = new PagingInfo();
query.PageInfo.Count = 1;
query.PageInfo.PageNumber = 1;

EntityCollection results = Helper.OrgService(organization).RetrieveMultiple(query);

if (results.Entities.Count > 0)
{
string lastNumber = results[0].Attributes["wit_nummer"].ToString();
if (lastNumber.Length < 9)
{
throw new InvalidProgramException("Die letzte Laufnummer war weniger als 9 Zeichen lang!");
}

string lastDayPattern = lastNumber.Substring(0, 6);
int lastCount = Convert.ToInt32(lastNumber.Substring(6, 3));
if (lastDayPattern == GetCurrentDayPattern())
{
lastCount++;
laufNummer = lastDayPattern + utils.StringHelper.FillLeftZero(lastCount, 3);
}
else
{
laufNummer = GetCurrentDayPattern() + suffix;
}

}
else
{
laufNummer = GetCurrentDayPattern() + suffix;
}

var entity = new Entity(EntityName);
entity.Attributes["neu_nummer"] = laufNummer;
EntityHelper.CreateEntity(organization, entity);

return laufNummer;
}

/// <summary>
/// Gets the current day pattern.
/// </summary>
/// <returns></returns>
public static string GetCurrentDayPattern()
{
return utils.StringHelper.FillLeftZero(Convert.ToInt32(DateTime.Now.Year.ToString().Substring(2, 2)), 2) +
utils.StringHelper.FillLeftZero(DateTime.Now.Month, 2) +
utils.StringHelper.FillLeftZero(DateTime.Now.Day, 2);
}
[/sourcecode]

Dieses Webservice kann man nun z.B. im OnSave-Event der Entität oder durch einen sonstigen Event triggern.
Viel Spaß beim Anwenden.

Cheers,
Chris

C# – Ausführenden Methodennamen zur Laufzeit auslesen

Ich hatte in einer Applikation die Notwendigkeit, ungeachtet der Performance, immer mitzuliefern, an welcher Stelle im Code ein Fehler auftritt bzw. aufgetreten ist – steinigt mich daher bitte nicht gleich für den Einsatz der Diagnostics-Klassen, welche bekanntermassen nicht die schnellsten sind!

Mittels folgendem Codesnippet kann man ganz einfach den Methodennamen und andere interessante Dinge auslesen:

[sourcecode language=“csharp“]
/// <summary>
/// Whoes the called me.
/// </summary>
/// <param name="frame">The frame.</param>
/// <returns></returns>
public static string GetCurrentFunctionName(int frame)
{
StackTrace stackTrace = new StackTrace();
StackFrame stackFrame = stackTrace.GetFrame(frame);
MethodBase methodBase = stackFrame.GetMethod();

return methodBase.ToString();
}

[/sourcecode]

Mit dem Parameter „frame“ gebt Ihr an, wieviele Methoden „zurückgesprungen“ werden soll. frame = 0 würde „GetCurrentFunctionName()“ retour liefern. frame = 1 liefert den Methodennamen, welcher die Methode „GetCurrentFunctionName()“ aufgerufen hat. Nach diesem Schema könnt Ihr entsprechend zurückspringen.

Cheers,
Chris

Office-Dokument in PDF/XPS umwandeln

Vielleicht hat ja jemand von euch meinen damaligen Blog gelesen. Wie ich dort geschrieben habe, ist die Generierung von PNG-Grafiken aus/für ein(em) Office-Dokument eine recht unpraktikable Geschichte. Alleine schon wg. der Limitierungen, die das PNG-Format mt sich bringt.

Es fiel daher die Entscheidung die Office-Dokumente (Word, Excel, Powerpoint) lediglich in XPS/PDF umwandeln. Um möglichst nahe am Microsoft Office zu bleiben, wurde entschieden die Interop-Assemblies (welche eine Office-Installation am Server vorraussetzen!!) zu verwenden. Diese bieten die Möglichkeit die Dokumente zu öffnen und als PDF/XPS abzuspeichern.

Nachfolgend mein Code dafür.

[sourcecode language=“csharp“]
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using Microsoft.Office.Interop.PowerPoint;
using Microsoft.Office.Interop.Word;
using Excel = Microsoft.Office.Interop.Excel;
using PowerPoint = Microsoft.Office.Interop.PowerPoint;
using Word = Microsoft.Office.Interop.Word;

namespace euernamespace
{
public class OfficeToXpsPdf
{
#region Properties & Constants

public static readonly ExtensionList WordExtensions = GetAllowedExtensions("Word");
public static readonly ExtensionList ExcelExtensions = GetAllowedExtensions("Excel");
public static readonly ExtensionList PowerpointExtensions = GetAllowedExtensions("Powerpoint");

public class ExtensionList : List<string>
{
public new bool Contains(string item)
{
bool rv = false;
rv = base.Contains(item);

if(!rv && item.Contains("."))
{
item = item.Replace(".", "").Trim();
rv = base.Contains(item);
}
return rv;
}
}

/// <summary>
/// Gets the allowed extensions.
/// </summary>
/// <param name="application">The application.</param>
/// <returns></returns>
public static ExtensionList GetAllowedExtensions(string application)
{
string configKey = "AllowedExtensions" + application;
string value = ConfigurationManager.AppSettings[configKey];
var list = new ExtensionList();

if (string.IsNullOrEmpty(value))
throw utils.Helper.PrepareException(new ConfigurationErrorsException("Setting ‚" + configKey + "‘ ist in der Config nicht gepflegt!"));

list.AddRange(value.Split(‚|‘).Where(extension => extension.Length > 1));
return list;
}

#endregion

#region enumerations
/// <summary>
/// Specifies all possible convert modes (xps & pdf)
/// </summary>
public enum OfficeConvertModes
{
Xps,
Pdf,
Unknown
}
#endregion

#region Public Methods
public static OfficeToXpsPdfConversionResult ConvertToXpsPdf(string sourceFilePath, OfficeConvertModes mode, ref string resultFilePath)
{
var result = new OfficeToXpsPdfConversionResult(ConversionResult.UnexpectedError);

// Check to see if it’s a valid file
if (!IsValidFilePath(sourceFilePath))
{
result.Result = ConversionResult.InvalidFilePath;
result.ResultText = sourceFilePath;
return result;
}

var ext = Path.GetExtension(sourceFilePath).ToLower();

// Check to see if it’s in our list of convertable extensions
if (!IsConvertableFilePath(sourceFilePath))
{
result.Result = ConversionResult.InvalidFileExtension;
result.ResultText = ext;
return result;
}

// Convert if Word
if (WordExtensions.Contains(ext))
{
return ConvertFromWord(sourceFilePath, mode, ref resultFilePath);
}

// Convert if Excel
if (ExcelExtensions.Contains(ext))
{
return ConvertFromExcel(sourceFilePath, mode, ref resultFilePath);
}

// Convert if PowerPoint
if (PowerpointExtensions.Contains(ext))
{
return ConvertFromPowerPoint(sourceFilePath, mode, ref resultFilePath);
}

return result;
}
#endregion

#region Private Methods
/// <summary>
/// Determines whether [is valid file path] [the specified source file path].
/// </summary>
/// <param name="sourceFilePath">The source file path.</param>
/// <returns>
/// <c>true</c> if [is valid file path] [the specified source file path]; otherwise, <c>false</c>.
/// </returns>
public static bool IsValidFilePath(string sourceFilePath)
{
if (string.IsNullOrEmpty(sourceFilePath))
return false;

try
{
return File.Exists(sourceFilePath);
}
catch (Exception)
{
}

return false;
}

/// <summary>
/// Determines whether [is convertable file path] [the specified source file path].
/// </summary>
/// <param name="sourceFilePath">The source file path.</param>
/// <returns>
/// <c>true</c> if [is convertable file path] [the specified source file path]; otherwise, <c>false</c>.
/// </returns>
public static bool IsConvertableFilePath(string sourceFilePath)
{
var ext = Path.GetExtension(sourceFilePath).ToLower();

return IsConvertableExtension(ext);
}

/// <summary>
/// Determines whether [is convertable extension] [the specified extension].
/// </summary>
/// <param name="extension">The extension.</param>
/// <returns>
/// <c>true</c> if [is convertable extension] [the specified extension]; otherwise, <c>false</c>.
/// </returns>
public static bool IsConvertableExtension(string extension)
{
return WordExtensions.Contains(extension) ||
ExcelExtensions.Contains(extension) ||
PowerpointExtensions.Contains(extension);
}

/// <summary>
/// Gets the temp XPS file path.
/// </summary>
/// <returns></returns>
private static string GetTempXpsFilePath()
{
return Path.ChangeExtension(Path.GetTempFileName(), ".xps");
}

/// <summary>
/// Converts from word.
/// </summary>
/// <param name="sourceFilePath">The source file path.</param>
/// <param name="mode">The mode.</param>
/// <param name="resultFilePath">The result file path.</param>
/// <returns></returns>
private static OfficeToXpsPdfConversionResult ConvertFromWord(string sourceFilePath, OfficeConvertModes mode, ref string resultFilePath)
{
object pSourceDocPath = sourceFilePath;

string pExportFilePath = string.IsNullOrWhiteSpace(resultFilePath) ? GetTempXpsFilePath() : resultFilePath;

try
{

WdExportFormat pExportFormat = Word.WdExportFormat.wdExportFormatXPS;
if (mode == OfficeConvertModes.Pdf)
pExportFormat = WdExportFormat.wdExportFormatPDF;

const bool pOpenAfterExport = false;
const WdExportOptimizeFor pExportOptimizeFor = Word.WdExportOptimizeFor.wdExportOptimizeForOnScreen;
const WdExportRange pExportRange = Word.WdExportRange.wdExportAllDocument;
const int pStartPage = 0;
const int pEndPage = 0;
const WdExportItem pExportItem = Word.WdExportItem.wdExportDocumentContent;
const bool pIncludeDocProps = true;
const bool pKeepIrm = true;
const WdExportCreateBookmarks pCreateBookmarks = Word.WdExportCreateBookmarks.wdExportCreateWordBookmarks;
const bool pDocStructureTags = true;
const bool pBitmapMissingFonts = true;
const bool pUseIso190051 = false;

Word.Application wordApplication = null;
Word.Document wordDocument = null;

try
{
wordApplication = new Word.Application();
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToInitializeOfficeApp, "Word", exc);
}

try
{
try
{
wordDocument = wordApplication.Documents.Open(ref pSourceDocPath);
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToOpenOfficeFile, exc.Message, exc);
}

if (wordDocument != null)
{
try
{
wordDocument.ExportAsFixedFormat(
pExportFilePath,
pExportFormat,
pOpenAfterExport,
pExportOptimizeFor,
pExportRange,
pStartPage,
pEndPage,
pExportItem,
pIncludeDocProps,
pKeepIrm,
pCreateBookmarks,
pDocStructureTags,
pBitmapMissingFonts,
pUseIso190051
);
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToExportToXps, "Word", exc);
}
}
else
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToOpenOfficeFile);
}
}
finally
{
// Close and release the Document object.
if (wordDocument != null)
{
wordDocument.Close();
wordDocument = null;
}

// Quit Word and release the ApplicationClass object.
if (wordApplication != null)
{
wordApplication.Quit();
wordApplication = null;
}

GC.Collect();
}
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToAccessOfficeInterop, "Word", exc);
}

resultFilePath = pExportFilePath;

return new OfficeToXpsPdfConversionResult(ConversionResult.Ok, pExportFilePath);
}

/// <summary>
/// Converts from power point.
/// </summary>
/// <param name="sourceFilePath">The source file path.</param>
/// <param name="mode">The mode.</param>
/// <param name="resultFilePath">The result file path.</param>
/// <returns></returns>
private static OfficeToXpsPdfConversionResult ConvertFromPowerPoint(string sourceFilePath, OfficeConvertModes mode, ref string resultFilePath)
{
string pSourceDocPath = sourceFilePath;

string pExportFilePath = string.IsNullOrWhiteSpace(resultFilePath) ? GetTempXpsFilePath() : resultFilePath;

try
{
PowerPoint.Application pptApplication = null;
PowerPoint.Presentation pptPresentation = null;

try
{
pptApplication = new PowerPoint.Application();
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToInitializeOfficeApp, "PowerPoint", exc);
}

try
{
try
{
pptPresentation = pptApplication.Presentations.Open(pSourceDocPath,
Microsoft.Office.Core.MsoTriState.msoTrue,
Microsoft.Office.Core.MsoTriState.msoTrue,
Microsoft.Office.Core.MsoTriState.msoFalse);
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToOpenOfficeFile, exc.Message, exc);
}

if (pptPresentation != null)
{
try
{

PpFixedFormatType exportType = PpFixedFormatType.ppFixedFormatTypeXPS;
if (mode == OfficeConvertModes.Pdf)
exportType = PpFixedFormatType.ppFixedFormatTypePDF;

pptPresentation.ExportAsFixedFormat(
pExportFilePath,
exportType,
PowerPoint.PpFixedFormatIntent.ppFixedFormatIntentScreen,
Microsoft.Office.Core.MsoTriState.msoFalse,
PowerPoint.PpPrintHandoutOrder.ppPrintHandoutVerticalFirst,
PowerPoint.PpPrintOutputType.ppPrintOutputSlides,
Microsoft.Office.Core.MsoTriState.msoFalse,
null,
PowerPoint.PpPrintRangeType.ppPrintAll,
string.Empty,
true,
true,
true,
true,
false
);
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToExportToXps, "PowerPoint", exc);
}
}
else
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToOpenOfficeFile);
}
}
finally
{
// Close and release the Document object.
if (pptPresentation != null)
{
pptPresentation.Close();
pptPresentation = null;
}

// Quit Word and release the ApplicationClass object.
if (pptApplication != null)
{
pptApplication.Quit();
pptApplication = null;
}

GC.Collect();
}
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToAccessOfficeInterop, "PowerPoint", exc);
}

resultFilePath = pExportFilePath;

return new OfficeToXpsPdfConversionResult(ConversionResult.Ok, pExportFilePath);
}

/// <summary>
/// Converts from excel.
/// </summary>
/// <param name="sourceFilePath">The source file path.</param>
/// <param name="mode">The mode.</param>
/// <param name="resultFilePath">The result file path.</param>
/// <returns></returns>
private static OfficeToXpsPdfConversionResult ConvertFromExcel(string sourceFilePath, OfficeConvertModes mode, ref string resultFilePath)
{
string pSourceDocPath = sourceFilePath;

string pExportFilePath = string.IsNullOrWhiteSpace(resultFilePath) ? GetTempXpsFilePath() : resultFilePath;

try
{
var pExportFormat = Excel.XlFixedFormatType.xlTypeXPS;
if (mode == OfficeConvertModes.Pdf)
pExportFormat = Excel.XlFixedFormatType.xlTypePDF;

var pExportQuality = Excel.XlFixedFormatQuality.xlQualityStandard;
var pOpenAfterPublish = false;
var pIncludeDocProps = true;
var pIgnorePrintAreas = true;

Excel.Application excelApplication = null;
Excel.Workbook excelWorkbook = null;

try
{
excelApplication = new Excel.Application();
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToInitializeOfficeApp, "Excel", exc);
}

try
{
try
{
excelWorkbook = excelApplication.Workbooks.Open(pSourceDocPath);
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToOpenOfficeFile, exc.Message, exc);
}

if (excelWorkbook != null)
{
try
{
excelWorkbook.ExportAsFixedFormat(
pExportFormat,
pExportFilePath,
pExportQuality,
pIncludeDocProps,
pIgnorePrintAreas,

OpenAfterPublish: pOpenAfterPublish
);
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToExportToXps, "Excel", exc);
}
}
else
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToOpenOfficeFile);
}
}
finally
{
// Close and release the Document object.
if (excelWorkbook != null)
{
excelWorkbook.Close();
excelWorkbook = null;
}

// Quit Word and release the ApplicationClass object.
if (excelApplication != null)
{
excelApplication.Quit();
excelApplication = null;
}

GC.Collect();
}
}
catch (Exception exc)
{
return new OfficeToXpsPdfConversionResult(ConversionResult.ErrorUnableToAccessOfficeInterop, "Excel", exc);
}

resultFilePath = pExportFilePath;

return new OfficeToXpsPdfConversionResult(ConversionResult.Ok, pExportFilePath);
}
#endregion
}

public enum ConversionResult
{
Ok = 0,
InvalidFilePath = 1,
InvalidFileExtension = 2,
UnexpectedError = 3,
ErrorUnableToInitializeOfficeApp = 4,
ErrorUnableToOpenOfficeFile = 5,
ErrorUnableToAccessOfficeInterop = 6,
ErrorUnableToExportToXps = 7
}
}
[/sourcecode]
OfficeToXpsPdf-Klasse

[sourcecode language=“csharp“]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace euernamespace
{
public class OfficeToXpsPdfConversionResult
{
#region Properties
public ConversionResult Result { get; set; }
public string ResultText { get; set; }
public Exception ResultError { get; set; }
#endregion

#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="OfficeToXpsPdfConversionResult"/> class.
/// </summary>
public OfficeToXpsPdfConversionResult()
{
Result = ConversionResult.UnexpectedError;
ResultText = string.Empty;
}

/// <summary>
/// Initializes a new instance of the <see cref="OfficeToXpsPdfConversionResult"/> class.
/// </summary>
/// <param name="result">The result.</param>
public OfficeToXpsPdfConversionResult(ConversionResult result)
: this()
{
Result = result;
}

/// <summary>
/// Initializes a new instance of the <see cref="OfficeToXpsPdfConversionResult"/> class.
/// </summary>
/// <param name="result">The result.</param>
/// <param name="resultText">The result text.</param>
public OfficeToXpsPdfConversionResult(ConversionResult result, string resultText)
: this(result)
{
ResultText = resultText;
}

/// <summary>
/// Initializes a new instance of the <see cref="OfficeToXpsPdfConversionResult"/> class.
/// </summary>
/// <param name="result">The result.</param>
/// <param name="resultText">The result text.</param>
/// <param name="exc">The exc.</param>
public OfficeToXpsPdfConversionResult(ConversionResult result, string resultText, Exception exc)
: this(result, resultText)
{
ResultError = exc;
}
#endregion
}
}

[/sourcecode]
Result-Klasse

Wie gesagt, vergesst beim Verwenden dieser Klassen nicht, dass Office (in meinem Fall 2010) und die dazugehörigen Primary Interop Assemblies (PIA) installiert sein müssen! Möchtet Ihr die Klassen in einem Webservice/in einer Webapplikation verwenden, so müsst Ihr zudem noch in den Komponentendiensten (comexp.msc) unter Komponentenliste -> Arbeitsplatz -> DCOM-Konfiguration für „Microsoft Excel Application“, „Microsoft PowerPoint-Folie“ und „Microsoft Word 97-2003-Dokument“ folgende Einstellung treffen:

Update:
Im laufenden Betrieb kam es immer wieder zur nachfolgenden Fehlermeldung:
Retrieving the COM class factory for component with CLSID {000209FF-0000-0000-C000-000000000046} failed due to the following error: 8000401a The server process could not be started because the configured identity is incorrect. Check the username and password.

Um das Problem zu beheben, musste ebenfalls in den DCOM-Konfigurations-Dialogen eine Einstellung gesetzt werden. Und zwar:

Was meint Ihr? Würdet Ihr noch etwas an den Klassen (abgesehen von kosmetischen Dingen) ändern?

Cheers,
Chris

CRM 2011 – jQueryUI Dialog verwenden

Was in normalen Web Applikationen ganz easy von der Hand geht, wird in CRM 2011 etwas schwierig. Wie bindet man z.B. das CSS der UI-Controls ein? Der Designer lässt ein Einbinden (wie bei den JS-Webresourcen) nämlich nicht zu. Auch kann man nicht so einfach vorgehen, wie in der jQueryUI-Dokumentation beschrieben.

Dort wird z.B. folgendes Beispiel für einen Modal-Dialog beschrieben:

[sourcecode language=“html“]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>jQuery UI Dialog – Basic modal</title>
<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">
<script src="../../jquery-1.6.2.js"></script>
<script src="../../external/jquery.bgiframe-2.1.2.js"></script>
<script src="../../ui/jquery.ui.core.js"></script>
<script src="../../ui/jquery.ui.widget.js"></script>
<script src="../../ui/jquery.ui.mouse.js"></script>
<script src="../../ui/jquery.ui.draggable.js"></script>
<script src="../../ui/jquery.ui.position.js"></script>
<script src="../../ui/jquery.ui.resizable.js"></script>
<script src="../../ui/jquery.ui.dialog.js"></script>
<link rel="stylesheet" href="../demos.css">
<script>
$(function() {
// a workaround for a flaw in the demo system (http://dev.jqueryui.com/ticket/4375), ignore!
$( "#dialog:ui-dialog" ).dialog( "destroy" );

$( "#dialog-modal" ).dialog({
height: 140,
modal: true
});
});
</script>
</head>
<body>

<div class="demo">

<div id="dialog-modal" title="Basic modal dialog">
<p>Adding the modal overlay screen makes the dialog look more prominent because it dims out the page content.</p>
</div>

<!– Sample page content to illustrate the layering of the dialog –>
<div class="hiddenInViewSource" style="padding:20px;">
<p>Sed vel diam id libero <a href="http://example.com">rutrum convallis</a>. Donec aliquet leo vel magna. Phasellus rhoncus faucibus ante. Etiam bibendum, enim faucibus aliquet rhoncus, arcu felis ultricies neque, sit amet auctor elit eros a lectus.</p>
<form>
<input value="text input" /><br />
<input type="checkbox" />checkbox<br />
<input type="radio" />radio<br />
<select>
<option>select</option>
</select><br /><br />
<textarea>textarea</textarea><br />
</form>
</div><!– End sample page content –>

</div><!– End demo –>

</body>
</html>

[/sourcecode]

Folgende Probleme stellen sich nun:
1.) Einbinden der JS-Files. Das geht gerade noch problemlos. Man muss lediglich Webresourcen vom Typ JScript hinzufügen (z.B. 1 für Jquery, 1 für JQueryUI; Für einen einfachen Dialog werden nur diese beiden Bibliotheken benötigt!) und diese im gewünschten Formular einfügen.

2.) Script-Block: Hier legt man ebenfalls am einfachsten eine Webresource vom Typ JScript an und ordnet sie dem Formular zu.

3.) Stylesheet: Man kann zwar eine Webresource vom Typ Stylesheet anlegen (wie bei JScript), diese aber nicht einem Fomular zuordnen. Das muss man selbst in einer Javascript-Webresource machen.

4.) Der HTML-Block mit der der ID „modal-dialog“ muss ins Form. Aber auch das ist nicht so ohne weiteres möglich. Auch hier ist es notwendig, den notwendigen HTML-Code in einer Javascript-Webresource zu erzeugen.

Daher ein kurzes Tutorial bzw. meine Lösung.

1.) Legt in eurer Solution zwei Webresourcen an und nennt diese am besten {euerprefix}_jQuery und {euerprefix}_jQueryUI und kopiert den Inhalt der Dateien jquery-1.6.2.min.js bzw. jquery-ui-1.8.16.custom.min.js in den Text-Editor.

2.) Legt eine weitere Resource, diesmal vom Typ „Stylesheet“ an und kopiert in diese den Inhalt der Datei jquery-ui-1.8.16.custom.css. Am unteren Ende des Dialogs steht eine URL. Diese bitte notieren.

3.) Legt eine weitere Webresource an und nennt diese {euerprefix}_{FormularName}.
In diese Resource habe ich die Funktionalitäten eingefügt, welche a) das Stylesheet laden und b) den notwendigen HTML-Code aufbauen.

[sourcecode language=“javascript“]
this.HideDialog = function () {
$("#dialog-modal").dialog(‚close‘)
}

// erstellt und öffnet den dialog
this.ShowDialog = function (title, message, detailsMessage, hideOkButton) {

// load stylesheet to head
var link = $("<link>");
link.attr({
type: ‚text/css‘,
rel: ’stylesheet‘,
<strong> href: "URL zum Stylesheet"</strong>
});
$("head").append(link);

var wrapper = $.create(‚div‘, { ‚id‘: ‚dialog-modal‘, ‚title‘: title }, []);
var text = $.create(’span‘, { ‚id‘: ‚dialogText‘ }, []);
text.innerHTML = message;
$(wrapper).append(text);

$(wrapper).append($.create(‚hr‘, {}, []));

var details = $.create(‚div‘, { ‚id‘: ‚divDialogDetails‘, ’style‘: ‚margin:3px; padding:7px; font-size:12px; display:block; background-color:#eeeeee; width:100%; height:100px;‘ }, []);
details.innerHTML = detailsMessage;
$(wrapper).append(details);

document.getElementById(‚tdAreas‘).appendChild(wrapper);

if (hideOkButton) {
$("#dialog-modal").dialog({
height: 400,
width: 400,
modal: true,
draggable: false,
resizable: false

});
}
else {
$("#dialog-modal").dialog({
height: 400,
width: 400,
modal: true,
draggable: false,
resizable: false,
buttons: { "schließen": function () { $(this).dialog("close"); } }
});
}
}
[/sourcecode]
Fügt bitte an der markierten Stelle die URL zu eurem Stylesheet ein!

$.create ruft ein jQuery-Plugin zum Erzeugen der DOM-Elemente auf. Ich habe den Code für das Plugin direkt in die Jquery-Webresource kopiert. Das könnt Ihr auch machen, oder legt einfach eine weitere JS-Resource an. Der Code für das Plugin lautet:

[sourcecode language=“javascript“]

// JQUERY Plugin zum Erzeugen von DOM-Elementen
jQuery.create = function () {
if (arguments.length == 0) return [];
var args = arguments[0] || {}, elem = null, elements = null;
var siblings = null;

// In case someone passes in a null object,
// assume that they want an empty string.
if (args == null) args = "";
if (args.constructor == String) {
if (arguments.length > 1) {
var attributes = arguments[1];
if (attributes.constructor == String) {
elem = document.createTextNode(args);
elements = [];
elements.push(elem);
siblings =
jQuery.create.apply(null, Array.prototype.slice.call(arguments, 1));
elements = elements.concat(siblings);
return elements;

} else {
elem = document.createElement(args);

// Set element attributes.
var attributes = arguments[1];
for (var attr in attributes)
jQuery(elem).attr(attr, attributes[attr]);

// Add children of this element.
var children = arguments[2];
children = jQuery.create.apply(null, children);
jQuery(elem).append(children);

// If there are more siblings, render those too.
if (arguments.length > 3) {
siblings =
jQuery.create.apply(null, Array.prototype.slice.call(arguments, 3));
return [elem].concat(siblings);
}
return elem;
}
} else return document.createTextNode(args);
} else {
elements = [];
elements.push(args);
siblings =
jQuery.create.apply(null, (Array.prototype.slice.call(arguments, 1)));
elements = elements.concat(siblings);
return elements;
}
};
[/sourcecode]

Die erzeugten HTML-Elemente werden dem DOM-Element „tdAreas“ hinzugefügt, welches jedes Formular hat und den Content des Forms hält. Wenn Ihr den Dialog auch anderswo verwenden möchtet, nehmt darauf Rücksicht!

So und zu guter letzt könnt Ihr den Dialog auch noch verwenden, wie seht Ihr hier…:

[sourcecode language=“javascript“]
ShowDialog(‚Titel‘, ‚Kurzbeschreibung‘, ‚längere Beschreibung‘);
HideDialog();
[/sourcecode]

Cheers,
Chris

CRM 2011 – Ribbon Button hinzufügen + Custom Rule

Christian Wagnsonner 1 Comment

Eine gängige Anforderung in einem CRM-Projekt ist es div. Schaltflächen den Ribbons hinzuzufügen, auszublenden oder zu aktivieren/deaktivieren.

Ich zeige euch in diesem Blog, wie man einen Button hinzufügt, der nur sichtbar ist, wenn die aufgerufene Entität bereits über eine ID verfügt (also bereits gespeichert wurde).

Zunächst muss man der Solution eine „Clienterweiterung“ hinzufügen, nämlich die „Anwendungsmenübänder“. Das geht wie folgt:

Danach ist es notwendig die Solution zu exportieren. Die heruntergeladene ZIP-Datei müsst Ihr irgendwohin extrahieren und die Datei „customizations.xml“ mit einem Editor öffnen. Ich empfehle euch dies direkt im Visual Studio zu tun, dort kann man auch recht einfach die Schema-Dateien zur Validierung einbinden. Wie das funktioniert, erfährt Ihr in folgendem Blog: Customizations XML validieren

Sucht nun in der Datei nach der Entität, welcher Ihr den Ribbon-Button hinzufügen möchtet. Solltet Ihr diese nicht finden, müsst Ihr diese zuerst der Solution hinzufügen und den Export wiederholen.

[sourcecode language=“xml“]
<Entity>
<Name LocalizedName="Lead" OriginalName="Lead">Lead</Name>
<ObjectTypeCode>4</ObjectTypeCode>
<strong><RibbonDiffXml></strong>
<CustomActions />
<Templates>
<RibbonTemplates Id="Mscrm.Templates"></RibbonTemplates>
</Templates>
<CommandDefinitions />
<strong><RuleDefinitions></strong>
<TabDisplayRules />
<DisplayRules />
<strong><EnableRules /></strong>
</RuleDefinitions>
<LocLabels />
</RibbonDiffXml>
</Entity>
[/sourcecode]

Das Beispiel zeigt eine Standard-Entität. Für dieses Beispiel ist nur die Bearbeitung der „RibbonDiffXml“-Sektion notwendig.
Zunächst fügt man eine CustomAction hinzu:

[sourcecode language=“xml“]
<CustomAction Id="Mscrm.Isv.wit_schnellerfassung.FormCustomAction" Location="Mscrm.Form.wit_schnellerfassung.MainTab.Groups._children" Sequence="6000">
<CommandUIDefinition>
<Group Id="Mscrm.Isv.wit_schnellerfassung.Form.Group0" Sequence="400" Command="Mscrm.Isv.wit_schnellerfassung.Form.Group0" Title="$LocLabels:CustomGroup.Title" Description="$LocLabels:CustomGroup.Description" Template="Mscrm.Templates.3.3">
<Controls Id="Mscrm.Isv.wit_schnellerfassung.Form.Group0.Controls">
<Button Id="Mscrm.Isv.wit_schnellerfassung.Form.Group0.Control0" Sequence="10" <strong>Command="Mscrm.Isv.wit_schnellerfassung.Form.Group0.Control0"</strong> Image16by16="$webresource:wit_icon_schnellerfassung" Image32by32="$webresource:wit_icon_schnellerfassung" TemplateAlias="o1"></Button>
</Controls>
</Group>
</CommandUIDefinition>
</CustomAction>
</CustomActions>
[/sourcecode]
Achtet auf die fett markierte Stelle. Der Command ist für das Hinzufügen der Rules (aktivieren/deaktivieren) wichtig. Die Attributes „Image16by16“ und „Image32by32“ zeigen auf Resourcen (in diesem Fall Icons), welche schon vorweg in der Solution sein müssen. Habt Ihr noch keine hinzugefügt, könnt Ihr diese Attributes vorerst auch leer lassen.

Nun sehen wir uns die „“-Sektion genauer an und fügen eine CommandDefinition hinzu:

[sourcecode language=“xml“]
<CommandDefinitions>
<CommandDefinition <strong>Id="Mscrm.Isv.wit_schnellerfassung.Form.Group0.Control0"></strong>
<EnableRules>
<EnableRule Id="schnellerfassungEnableDisable"></EnableRule>
</EnableRules>
<DisplayRules />
<Actions>
<JavaScriptFunction Library="$webresource:wit_schnellerfassung" FunctionName="Generate" />
</Actions>
</CommandDefinition>
</CommandDefinitions>
[/sourcecode]

Die fett markierte ID der CommandDefinition ist ident mit dem Command-Attribut von vorhin!! Achtet darauf, dass dies auch bei euch ident ist.
Unter EnableRules fügen wir nun eine Rule hinzu. Auch hier wird die ID später noch benötigt!! Unter Actions fügen wir eine Javascript Action hinzu. In diesem Fall soll bei Klick auf den Button die Funktion „GenerateName“ aus der WebResource „wit_schnellerfassung“ aufgerufen werden.

Und zu guter letzt muss noch die -Sektion angepasst werden:

[sourcecode language=“xml“]
<EnableRules>
<EnableRule Id="schnellerfassungEnableDisable">
<CustomRule FunctionName="ShowHideGenerateButton" Library="$webresource:wit_schnellerfassung"></CustomRule>
</EnableRule>
</EnableRules>
</RuleDefinitions>
[/sourcecode]

Die ID der EnableRule muss mit jener von vorhin übereinstimmen! Die CustomRule ruft wieder eine Javascript-Funktion aus der Resource „wit_schnellerfassung“ auf, welche true/false retournieren muss.

In meinem Fall sieht diese Funktion (noch) wie folgt aus:

[sourcecode language=“javascript“]
function ShowHideGenerateButton() {
var entityId = Xrm.Page.data.entity.getId();
if (!Utils.StringIsNullOrEmpty(entityId)) {
return true;
}
else {
return false;
}
}
[/sourcecode]

Viel Spaß beim Nachmachen.

Happy Programming!

CRM 2011 – Customizations.xml – Validieren!

Christian Wagnsonner 1 Comment

Ein Problem, das vielen vermutlich schon einige Produktivstunden gekostet hat, ist wohl das Erweitern bzw. Validieren der Solution-Customizations.xml-Datei. Wenn man da mal eine Menge an angepassten Entitäten drin hat, wird es schnell unübersichtlich und das Anpassen eine Tortur.

Am einfachsten hilft man sich, in dem man die Schema-Dateien (Teil der SDK) herunterlädt und im Visual Studio einbindet. Im Installations-Ordner der SDK befindet sich ein Ordner „schemas“. Öffnet nun einfach euer Visual Studio und bindet die Schema-Dateien mittels folgenden Schritten ein:

* XML –>
* Schemas… –>
* Add…

Und weil es mich gerade einiges an Zeit gekostet hat: Achtet auf die Sensitivity. Id != id.

Happy Programming!

CRM 2011 – Custom View/Lookup aufbauen und filtern

Christian Wagnsonner 4 Comments

Wenn man etwas mit CRM arbeitet, stößt man zwangsläufig auf einige Limitierungen. Einfaches Beispiel: Man hat eine Verkaufschance und darunter eine Entität, z.B. Produkt (1:n). Dieses Produkt hat jedoch eine weitere Entität darunter, z.B. Produktpositionen (wieder 1:n). Auf der Form für Produktpositionen möchte ich jetzt als Lookup das Produkt auswählen. CRM listet in diesem Fall nun sämtliche Produkte auf und nicht nur jene, die unter der Verkaufschance hängen.

In so einem Fall kann man einen eigenen View (Stichwort CustomView) zur Laufzeit erstellen und dem Lookup zuordnen.
Hierfür habe ich mir eine Helper-Funktion geschrieben, welcher man u.a. die dynamisch die Spalten des Lookups erstellt, Filter/Ordering setzt und die Auswahl eines anderen Views im Lookup unterbindet.

[sourcecode language=“javascript“]

function SetViewForProjektProdukt() {
var lookupEntity = "lookupentity"; // Name der Entität, die im Lookup angezeigt werden soll
var lookupEntityPK = "lookupEntityPK "; // PrimaryKey der Entität, die im Lookup angezeigt werden soll
var lookupFieldId = "lookupFieldId "; // ID des Lookups im Formular, für das der CustomView eingerichtet werden soll
var lookupName = "lookupName"; // Bezeichnung des Lookups

var lookupFormColumns = new Array(); // Spalten welche im Lookup angezeigt werden sollen, müssen Teil der Entität "lookupentity" sein
lookupFormColumns[0] = "new_attr1";
lookupFormColumns[1] = "new_attr2";

var filterAttribute = "new_attr1";
var filterValue = ‚{GUID}‘;

var orderAttribute = "createdon"; // Feld, nach dem sortiert werden soll
var orderDescending = "true"; // asc/desc

SetLookupForm(lookupFieldId, lookupEntity, lookupEntityPK, lookupFormColumns, lookupName, filterAttribute, filterValue, orderAttribute, orderDescending);
}

function SetLookupForm(lookupFieldId, lookupEntity, lookupEntityPK, lookupFormColumns, lookupName, filterAttribute, filterValue, orderAttribute, orderDescending) {

if (Utils.StringIsNullOrEmpty(orderAttribute)) {
orderAttribute = "createdon";
}

if (Utils.StringIsNullOrEmpty(orderDescending)) {
orderDescending = "false";
}

//Note: in the form designer make sure the lookup field is set to "Show all views" in its "View Selector" property
//Set parameters values needed for the creation of a new lookup view…
var viewId = Xrm.Page.context.getUserId(); // Your new lookup views needs a unique id. It must be a GUID. Here I use the GUID of the current user id.
var entityName = lookupEntity; // The entity your new lookup view relates to
var viewDisplayName = lookupName; // A name for new lookup view
var viewIsDefault = true; // Whether your new lookup view should be the default view displayed in the lookup or not

//Define the Fetch XML query your new lookup view will use. You can create this via Advanced Find. You’ll need to massage the syntax a little though
var fetchXml =
"<fetch version=’1.0′ output-format=’xml-platform‘ mapping=’logical‘ distinct=’false‘>" +
"<entity name=’" + lookupEntity + "‘>";

for (var i = 0; i < lookupFormColumns.length; i++) {
fetchXml = fetchXml + "<attribute name=’" + lookupFormColumns[i] + "‘ />";
}

fetchXml = fetchXml +
"<filter type=’and‘>" +
"<condition attribute=’" + filterAttribute + "‘ operator=’eq‘ value=’" + filterValue + "‘ />" +
"</filter>" +
"<order attribute=’" + orderAttribute + "‘ decending=’" + orderDescending + "‘ />" +
"</entity>" +
"</fetch>";

//Define the appearance of your new lookup view
var layoutXml =
"<grid name=’resultset‘ object=’1′ jump=’name‘ select=’1′ icon=’1′ preview=’1′>" +
"<row name=’result‘ id=’" + lookupEntityPK + "‘>"; // id = the GUID field from your view that the lookup should return to the CRM form

for (var i = 0; i < lookupFormColumns.length; i++) {
layoutXml = layoutXml + "<cell name=’" + lookupFormColumns[i] + "‘ width=’100′ />";
}

layoutXml = layoutXml +
"</row>" +
"</grid>";

//Add your new view to the Lookup’s set of availabe views and make it the default view
$("#" + lookupFieldId).attr("disableViewPicker", "0");
Xrm.Page.getControl(lookupFieldId).addCustomView(viewId, entityName, viewDisplayName, fetchXml, layoutXml, viewIsDefault);
$("#" + lookupFieldId).attr("disableViewPicker", "1");

}
[/sourcecode]

Ich hoffe, ich kann euch damit ein paar Stunden eurer kostbaren Zeit sparen 😉

Cheers,
Chris

CRM 2011 – Attribut-Wert setzen (auch bei disabled fields)

Wenn man etwas mit CRM 2011 arbeitet, stellt man schnell fest, dass die Werte in Textboxen und anderen Controls nicht gespeichert werden, wenn man diese mit „document.getElementByid().value“ bearbeitet. Grund dafür ist, dass ein Event (der das Element zum Speichern markiert) nicht subscribed wird.
Wenn das Feld zudem noch als readonly markiert ist, passiert gar nichts. Insofern kann ich euch meine Helper-Methode ans Herz legen, welche in beiden Fällen ihre Dienste verrichtet:

[sourcecode language=“javascript“]
// Setzt den Wert eines "normalen" Attributes auf einer Entity-Page und speichert diesen ab (=&gt; refresh der Page)
this.SaveAttributeValue = function SaveAttributeValue(id, value) {

var isDisabledField = Xrm.Page.getControl(id).getDisabled();

if (isDisabledField) {
Xrm.Page.getControl(id).setDisabled(false);
}

Xrm.Page.getAttribute(id).setValue(value);
if (Xrm.Page.data.entity.getId() != null) {
Xrm.Page.data.entity.save();
}

if (isDisabledField) {
Xrm.Page.getControl(id).setDisabled(true);
}
}
[/sourcecode]

Viel Spaß damit!

Cheers,
Chris

CRM 2011 – Text/Value von Lookup auslesen

Wie man Text/Value von CRM-Elementen (via Javascript) ausliest, ist recht einfach. Etwas komplizierter wird es jedoch, wenn man dies bei Lookups machen möchte. Aus diesem Grund habe ich mir zwei Helper-Methoden geschrieben:

[sourcecode language=“javascript“]
// Gibt aus dem Lookup den Text (value) für den aktuell ausgewählten Wert retour.
// Sollte das Lookup nicht gefunden werden oder noch kein Wert ausgewählt worden sein, wird ein Leerstring retour gegeben.
this.GetTextFromLookup = function GetTextFromLoopup(id) {
try {
return Xrm.Page.getAttribute(id).getValue()[0].name;
}
catch (e) {
return "";
}
}

// Gibt aus dem Lookup den GUID (value) für den aktuell ausgewählten Wert retour.
// Sollte das Lookup nicht gefunden werden oder noch kein Wert ausgewählt worden sein, wird ein Leerstring retour gegeben.
this.GetValueFromLookup = function GetValueFromLookup(id) {
try {
return Xrm.Page.getAttribute(id).getValue()[0].name.id;
}
catch (e) {
return "";
}
}
[/sourcecode]

Was Ihr im Catch-Block macht, überlasse ich euch.

Ich hoffe, ich konnte euch ein wenig weiterhelfen!

Cheers,
Chris

CRM 2011 – Feld Validierung via Javascript setzen

Beim Designen von div. Forms kann man schon mal auf die Notwendigkeit stossen, ein bestimmtes Feld nur unter gewissen Umständen validieren zu lassen.
Das geht in CRM 2011 recht einfach:

[sourcecode language=“javascript“]
Xrm.Page.getAttribute("feldname").setRequiredLevel("required");
[/sourcecode]

Mögliche Levels sind:

  • requird
  • recommended
  • none

Viel Spaß beim CRM coden!

Cheers,
Chris