How to Email an Entire Log File through Nlog - vb.net

Salvete! nLog for .NET has the capability to send log entries as email. But if we want to send the entire current log file, how can it be done, short of reading the log file into a string and passing that as an nLog {$message}? I don't see that nLog has a property in its mailtarget for attachments. How can this be done?

I just did this today in C#. I followed the answer provided by arkmuetz here How to get path of current target file using NLog in runtime? to first get the filename of the Nlog target. I then read the file and copied the contents of the file to another Nlog target which emailed the log in the body of the message.
First I created an NlogHelper Class
public static class NlogHelper
{
/// <summary>
/// Gets LogFileName by TargetName
/// </summary>
/// <param name="targetName">The nLog targetname for the specified logger.</param>
/// <returns></returns>
/// <remarks>https://stackoverflow.com/questions/11452645/how-to-get-path-of-current-target-file-using-nlog-in-runtime</remarks>
public static string GetLogFileName(string targetName)
{
string fileName = null;
if (LogManager.Configuration != null && LogManager.Configuration.ConfiguredNamedTargets.Count != 0)
{
Target target = LogManager.Configuration.FindTargetByName(targetName);
if (target == null)
{
throw new Exception("Could not find target named: " + targetName);
}
FileTarget fileTarget = null;
WrapperTargetBase wrapperTarget = target as WrapperTargetBase;
// Unwrap the target if necessary.
if (wrapperTarget == null)
{
fileTarget = target as FileTarget;
}
else
{
fileTarget = wrapperTarget.WrappedTarget as FileTarget;
}
if (fileTarget == null)
{
throw new Exception("Could not get a FileTarget from " + target.GetType());
}
var logEventInfo = new LogEventInfo { TimeStamp = DateTime.Now };
fileName = fileTarget.FileName.Render(logEventInfo);
}
else
{
throw new Exception("LogManager contains no Configuration or there are no named targets");
}
//if (!File.Exists(fileName))
//{
// throw new Exception("File " + fileName + " does not exist");
//}
return fileName;
}
}
Next I retrieved the file name in the constructor and stored it for later use.
_logLileName = NlogHelper.GetLogFileName("DailyFile");
For my purposes I created property that I later used to retrieve the contents of the log file.
public string LogFileContent
{
get
{
return File.ReadAllText(_logLileName);
}
}
Finally I added the contents of the file to the log to be emailed.
_logger.Error("Excel Builder LinkShare Error encountered -- Some companies did not complete succesfully!. Please review logs. \n" + mfb.LogFileContent);
Here is my nLog.config file thought will help make things more clear.
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >
<targets>
<target name="console" xsi:type="ColoredConsole"
layout="${date:format=HH\:mm\:ss}|${level}|${stacktrace}|${message}" />
<target name="file" xsi:type="File" fileName="${basedir}/DomoExcelBuilderLog.txt"
layout="${stacktrace} ${message}" />
<target name="DailyFile" xsi:type="File" fileName="C:\Temp\Log${shortdate}.txt"
layout="${date:format=HH\:mm\:ss} --> ${message}" deleteOldFileOnStartup="true" />
<target name="file2" xsi:type="File" fileName="${basedir}/ServiceLog.txt"
layout="${stacktrace} ${message}" />
<target name="MyMailer" xsi:type="Mail"
smtpServer="some.server.com"
smtpPort="587"
smtpAuthentication="Basic"
smtpUsername="no-reply"
smtpPassword="somepassword"
enableSsl="true"
from="no-reply#some.server.com"
to="myemail#gmail.com"/>
</targets>
<rules>
<logger name="MultipleFileBuilder" minlevel="Trace" writeTo="DailyFile" />
<logger name="ExcelBuilderService" minlevel="Debug" writeto="MyMailer" />
</rules>
</nlog>
My application first logs to the logger named 'MultipleFileBuilder'. I then use the NLogHelper class to retrieve the contents of the Target 'DailiyFile'.
Hope this helps someoneout.
(NOTE: I removed the checking to see if the file exists in the 'GetLogFileName' method because in my case file didn't exist.)

I think you may use a combination of targets: default target + MethodCall and then send email manually or use Mail + Buffering to send a batch of records.

Related

WinUI notifications: COM exception is raised when instanciating instance of "AppNotification"

I wanted to include notifications in my existing WinUI 3 application which uses Windows App SDK 1.1.4 and .NET 6 (The application does not and shall not use packaging / MSIX).
In order to achieve this, I tried to extract some code of an example application that I created with the "Template studio for WinUI" project template (assistant), see https://github.com/microsoft/TemplateStudio/ (The sample application also works with the 'unpackaged' deployment model).
The code which I extracted from the example application looks like this (the relevant parts should be the methods 'Initialize' and 'Show'):
public class AppNotificationService : IAppNotificationService
{
public AppNotificationService()
{
}
~AppNotificationService()
{
Unregister();
}
public void Initialize()
{
AppNotificationManager.Default.NotificationInvoked += OnNotificationInvoked;
AppNotificationManager.Default.Register();
}
public void OnNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args)
{
// TODO: Handle notification invocations when your app is already running.
//// // Navigate to a specific page based on the notification arguments.
//// if (ParseArguments(args.Argument)["action"] == "Settings")
//// {
//// App.MainWindow.DispatcherQueue.TryEnqueue(() =>
//// {
//// _navigationService.NavigateTo(typeof(SettingsViewModel).FullName!);
//// });
//// }
App.MainWindow.DispatcherQueue.TryEnqueue(() =>
{
App.MainWindow.ShowMessageDialogAsync("TODO: Handle notification invocations when your app is already running.", "Notification Invoked");
App.MainWindow.BringToFront();
});
}
// EXCEPTION IN THIS METHOD
public bool Show(string payload)
{
var appNotification = new AppNotification(payload); // COM EXCEPTION HERE
AppNotificationManager.Default.Show(appNotification);
return appNotification.Id != 0;
}
public NameValueCollection ParseArguments(string arguments)
{
return HttpUtility.ParseQueryString(arguments);
}
public void Unregister()
{
AppNotificationManager.Default.Unregister();
}
}
As you can see, the code contains a method "Show" that has a string for the payload that represents the notification message. In addition there is a "Initialize" method that the example code calls upon application startup.
In order to call the "Show" method of the code above, I created some small event handler in my application that gets called when I click a button:
private void CreateNotification_Click(object sender, RoutedEventArgs e)
{
AppNotificationService notificationService = new AppNotificationService();
notificationService.Initialize();
string notificationContent = "test";
notificationService.Show(notificationContent);
}
However, the call to "notificationService.Show(notificationContent);" always causes a ComException "0xC00CE556" that is raised when the code tries to instanciate the AppNotification instance see here:
I do not know what I am missing here. It seems that the template studio application does something additional to get the notification working, that I am currently not doing in my code. But I have no idea what that is. Any suggestions?
I couldn't reproduce your COM Exception but these steps worked.
Create a simple WinUI 3 app project.
Bring AppNotificationService.cs and IAppNotificationService.cs from a TemplateStudio project with app notifications.
Open Package.appxmanifest using a text editor (VSCode).
Add these namespaces:
<Package
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10">
</Package>
Declare these Extensions inside Applications:
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements DisplayName="WinUI3BlankAppProjectTemplate" Description="WinUI3BlankAppProjectTemplate" BackgroundColor="transparent" Square150x150Logo="Assets\Square150x150Logo.png" Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<!--Specify which CLSID to activate when notification is clicked-->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="12345678-9ABC-DEFG-HIJK-LMNOPQRSTUVW" />
</desktop:Extension>
<!--Register COM CLSID-->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="AppNotifications.exe" Arguments="----AppNotificationActivated:" DisplayName="Toast activator">
<com:Class Id="12345678-9ABC-DEFG-HIJK-LMNOPQRSTUVW" DisplayName="Toast activator" />
</com:ExeServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Application>
</Applications>
Replace Executable="AppNotifications.exe" with your app name.
Create a GUID from [Tools]-[Create GUID] in VisualStudio menu.
Replace the two GUIDs in the Extensions with the GUID you created.
Save the Package.appxmanifest file and reopen and rebuild the solution.
Call the Show method passing a valid payload. For example:
var xmlPayload =
#"
<toast launch=""action=ToastClick"">
<visual>
<binding template=""ToastGeneric"">
<text>App Notification</text>
<text></text>
</binding>
</visual>
<actions>
<action content=""Settings"" arguments=""action=Settings""/>
</actions>
</toast>
";
appNotificationService.Show(xmlPayload);
UPDATE
For un-packaged(non-packaged) apps you get a COM Exception if you don't call the Initialize() method. So, the step 10. should be something like this:
Call Initialize then Show method passing a valid payload. For example:
AppNotificationService appNotificationService = new();
appNotificationService.Initialize();
var xmlPayload =
#"
<toast launch=""action=ToastClick"">
<visual>
<binding template=""ToastGeneric"">
<text>App Notification</text>
<text></text>
</binding>
</visual>
<actions>
<action content=""Settings"" arguments=""action=Settings""/>
</actions>
</toast>
";
appNotificationService.Show(xmlPayload);
I found out what the problem was in my case.
I turned out, that the problem was related to the string I used as notification content. This must not be an arbitrary string (like "test" in the example I used in the question), but an xml string that has a specific format which is needed to represent a "toast" message.
This xml string for the toast message can contain specific elements for text, images and buttons that may appear in the message.
Simple example:
<?xml version="1.0" encoding="utf-8"?>
<toast>
<visual>
<binding template="ToastGeneric">
<text>Some text</text>
</binding>
</visual>
</toast>
This article shows an example of the possible syntax of this xml string:
Quickstart: App notifications in the Windows App SDK - 4 Display an app notification
There are also classes and helpers for the construction of the xml string that you can use by installing the "CommunityToolkit.WinUI.Notifications"
Nuget package:
ToastContent class
ToastContentBuilder class
Example code
Basically, all you need to do to create a toast notification is this:
private void CreateNotification_Click(object sender, RoutedEventArgs e)
{
// Version 1: Directly define the xmlPayload string:
// string xmlPayload = #"<?xml version=""1.0"" encoding=""utf-8""?><toast><visual><binding template=""ToastGeneric""><text>Some text</text></binding></visual></toast>";
// Version 2: Create the the xmlPayload string using the ToastContentBuilder
// ToastContentBuilder comes with the "CommunityToolkit.WinUI.Notifications" Nuget package
ToastContent toastContent = new ToastContentBuilder()
.AddText("Some text")
.GetToastContent();
string xmlPayload = toastContent.GetContent();
var toast = new AppNotification(xmlPayload);
AppNotificationManager.Default.Show(toast);
}
In the example code, I have two versions on how to create the xml string that represents the toast message. You can create the xml yourself or use the ToastContentBuilder class from the "CommunityToolkit.WinUI.Notifications" Nuget package.
Personal opinion
I know that StackOverflow does no focus on personal opinions. However, I would like to express that I am quite disappointed to see that a code like
new AppNotification("test");
raises an ComException lacking any useful information instead of an meaningful exception that contains some hint for the developer complaining about the incorrect format of the provided xml string.

Fetch id from url in block file

What is the standard of Magento for fetch id from URL in the block file?
My URL is http://test.com/quote/savequote/view/id/7/
My XML file
<quote_savequote_view>
<label>View Quote</label>
<reference name="root">
<action method="setTemplate">
<template>page/1column.phtml</template>
</action>
</reference>
<reference name="content">
<block type="quote/savequote" name="quote.savequote" template="quote/view.phtml" />
</reference>
</quote_savequote_view>
Controller Action
public function viewAction(){
$this->loadLayout();
$this->renderLayout();
}
Block File
public function getDetails(){
// I want to get id here and I want to use the id in collection
}
Please help me what is the standard.
Finally I have solution, We can use mage register for pass data between block and controller.
public function viewAction(){
$id = Mage::app()->getRequest()->getParam('id');
$results = // your model results
Mage::register('quotedetails', $results);
$this->loadLayout();
$this->renderLayout();
}
Block File
public function getDetails(){
return Mage::registry('quotedetails');
}

Multiple Applications Reference Same .DLL [duplicate]

My DLLs are loaded by a third-party application, which we can not customize. My assemblies have to be located in their own folder. I can not put them into GAC (my application has a requirement to be deployed using XCOPY).
When the root DLL tries to load resource or type from another DLL (in the same folder), the loading fails (FileNotFound).
Is it possible to add the folder where my DLLs are located to the assembly search path programmatically (from the root DLL)? I am not allowed to change the configuration files of the application.
Sounds like you could use the AppDomain.AssemblyResolve event and manually load the dependencies from your DLL directory.
Edit (from the comment):
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += new ResolveEventHandler(LoadFromSameFolder);
static Assembly LoadFromSameFolder(object sender, ResolveEventArgs args)
{
string folderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll");
if (!File.Exists(assemblyPath)) return null;
Assembly assembly = Assembly.LoadFrom(assemblyPath);
return assembly;
}
You can add a probing path to your application's .config file, but it will only work if the probing path is a contained within your application's base directory.
Update for Framework 4
Since Framework 4 raise the AssemblyResolve event also for resources actually this handler works better. It's based on the concept that localizations are in app subdirectories (one for localization with the name of the culture i.e. C:\MyApp\it for Italian)
Inside there are resources file.
The handler works also if the localization is country-region i.e. it-IT or pt-BR. In this case the handler "might be called multiple times: once for each culture in the fallback chain" [from MSDN]. This means that if we return null for "it-IT" resource file the framework raises the event asking for "it".
Event hook
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += new ResolveEventHandler(currentDomain_AssemblyResolve);
Event handler
Assembly currentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
//This handler is called only when the common language runtime tries to bind to the assembly and fails.
Assembly executingAssembly = Assembly.GetExecutingAssembly();
string applicationDirectory = Path.GetDirectoryName(executingAssembly.Location);
string[] fields = args.Name.Split(',');
string assemblyName = fields[0];
string assemblyCulture;
if (fields.Length < 2)
assemblyCulture = null;
else
assemblyCulture = fields[2].Substring(fields[2].IndexOf('=') + 1);
string assemblyFileName = assemblyName + ".dll";
string assemblyPath;
if (assemblyName.EndsWith(".resources"))
{
// Specific resources are located in app subdirectories
string resourceDirectory = Path.Combine(applicationDirectory, assemblyCulture);
assemblyPath = Path.Combine(resourceDirectory, assemblyFileName);
}
else
{
assemblyPath = Path.Combine(applicationDirectory, assemblyFileName);
}
if (File.Exists(assemblyPath))
{
//Load the assembly from the specified path.
Assembly loadingAssembly = Assembly.LoadFrom(assemblyPath);
//Return the loaded assembly.
return loadingAssembly;
}
else
{
return null;
}
}
The best explanation from MS itself:
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += new ResolveEventHandler(MyResolveEventHandler);
private Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
//This handler is called only when the common language runtime tries to bind to the assembly and fails.
//Retrieve the list of referenced assemblies in an array of AssemblyName.
Assembly MyAssembly, objExecutingAssembly;
string strTempAssmbPath = "";
objExecutingAssembly = Assembly.GetExecutingAssembly();
AssemblyName[] arrReferencedAssmbNames = objExecutingAssembly.GetReferencedAssemblies();
//Loop through the array of referenced assembly names.
foreach(AssemblyName strAssmbName in arrReferencedAssmbNames)
{
//Check for the assembly names that have raised the "AssemblyResolve" event.
if(strAssmbName.FullName.Substring(0, strAssmbName.FullName.IndexOf(",")) == args.Name.Substring(0, args.Name.IndexOf(",")))
{
//Build the path of the assembly from where it has to be loaded.
strTempAssmbPath = "C:\\Myassemblies\\" + args.Name.Substring(0,args.Name.IndexOf(","))+".dll";
break;
}
}
//Load the assembly from the specified path.
MyAssembly = Assembly.LoadFrom(strTempAssmbPath);
//Return the loaded assembly.
return MyAssembly;
}
For C++/CLI users, here is #Mattias S' answer (which works for me):
using namespace System;
using namespace System::IO;
using namespace System::Reflection;
static Assembly ^LoadFromSameFolder(Object ^sender, ResolveEventArgs ^args)
{
String ^folderPath = Path::GetDirectoryName(Assembly::GetExecutingAssembly()->Location);
String ^assemblyPath = Path::Combine(folderPath, (gcnew AssemblyName(args->Name))->Name + ".dll");
if (File::Exists(assemblyPath) == false) return nullptr;
Assembly ^assembly = Assembly::LoadFrom(assemblyPath);
return assembly;
}
// put this somewhere you know it will run (early, when the DLL gets loaded)
System::AppDomain ^currentDomain = AppDomain::CurrentDomain;
currentDomain->AssemblyResolve += gcnew ResolveEventHandler(LoadFromSameFolder);
I've used #Mattias S' solution. If you actually want to resolve dependencies from the same folder - you should try using Requesting assembly location, as shown below. args.RequestingAssembly should be checked for nullity.
System.AppDomain.CurrentDomain.AssemblyResolve += (s, args) =>
{
var loadedAssembly = System.AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName == args.Name).FirstOrDefault();
if(loadedAssembly != null)
{
return loadedAssembly;
}
if (args.RequestingAssembly == null) return null;
string folderPath = Path.GetDirectoryName(args.RequestingAssembly.Location);
string rawAssemblyPath = Path.Combine(folderPath, new System.Reflection.AssemblyName(args.Name).Name);
string assemblyPath = rawAssemblyPath + ".dll";
if (!File.Exists(assemblyPath))
{
assemblyPath = rawAssemblyPath + ".exe";
if (!File.Exists(assemblyPath)) return null;
}
var assembly = System.Reflection.Assembly.LoadFrom(assemblyPath);
return assembly;
};
I came here from another (marked duplicate) question about adding the probing tag to the App.Config file.
I want to add a sidenote to this - Visual studio had already generated an App.config file, however adding the probing tag to the pregenerated runtime tag did not work! you need a seperate runtime tag with the probing tag included. In short, your App.Config should look like this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Text.Encoding.CodePages" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<!-- Discover assemblies in /lib -->
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="lib" />
</assemblyBinding>
</runtime>
</configuration>
This took some time to figure out so I am posting it here. Also credits to The PrettyBin NuGet Package. It is a package that moves the dlls automatically. I liked a more manual approach so I did not use it.
Also - here is a post build script that copies all .dll/.xml/.pdb to /Lib. This unclutters the /debug (or /release) folder, what I think people try to achieve.
:: Moves files to a subdirectory, to unclutter the application folder
:: Note that the new subdirectory should be probed so the dlls can be found.
SET path=$(TargetDir)\lib
if not exist "%path%" mkdir "%path%"
del /S /Q "%path%"
move /Y $(TargetDir)*.dll "%path%"
move /Y $(TargetDir)*.xml "%path%"
move /Y $(TargetDir)*.pdb "%path%"
look into AppDomain.AppendPrivatePath (deprecated) or AppDomainSetup.PrivateBinPath

Sitecore Pipeline (indexing.filterIndex.inbound) not being called

I am trying to create a Lucene index in Sitecore 8.x of items that are visible to unauthenticated users (extranet\Anonymous). In order to do this I am trying to use the indexing.filterIndex.inbound pipeline.
I have tried writing a custom pipeline that returns false if the item cannot be read as extranet\Anonymous:
public class ApplyInboundIndexAccessFilter : InboundIndexFilterProcessor
{
public override void Process(InboundIndexFilterArgs args)
{
var item = args.IndexableToIndex as SitecoreIndexableItem;
var anonymousUser = Sitecore.Security.Accounts.User.FromName("extranet\\anonymous", false);
if (!item.Item.Security.CanRead(anonymousUser))
{
args.IsExcluded = true;
}
}
}
but at no time does this pipeline get invoked.
I have added my config (tried it with the default, before, after, with the default removed)
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<indexing.filterIndex.inbound>
<processor type="MyApplication.Site.Features.ContentSearch.IndexFilters.ApplyInboundIndexAccessFilter, MyApplication.Site">
<includedIndexNames hint="list">
<indexName>siteSearchIndex_web</indexName>
</includedIndexNames>
<excludedIndexNames hint="list">
<indexName>siteSearchIndex_master</indexName>
</excludedIndexNames>
</processor>
</indexing.filterIndex.inbound>
</pipelines>
</sitecore>
</configuration>
Am I right in assuming that this should be called on indexing, if not, when?
Any suggestions would be gratefully received.

WIX: don't show build number from [ProductVersion]

My Wix installer has the product version set like this:
<Product Version="4.6.0.0" ..>
And I show it in the UI like this:
<String Id="WelcomeDlgTitle">{\WixUI_Font_Bigger}Welcome to the [ProductName] v[ProductVersion] Setup Wizard</String>
This works well but I would like to omit the build number (ie. v4.6.0 instead of v4.6.0.0). Is there a way to do this?
You can omit the 4th field of the Product/#Version attribute as Windows Installer ignores it anyway.
<Product Version="4.6.0" ..>
If you really want to keep the 4th field of the version number, you'd have to write a custom action to parse the string and strip the 4th field.
Thank you #zett42, your comment pointed me out in the right direction - I created a Wix Preprocessor Extension, following the documentation here: http://wixtoolset.org/documentation/manual/v3/wixdev/extensions/extension_development_preprocessor.html
Here are the classes I created:
public class MaterialiserExtension : WixExtension
{
private MaterialiserPreprocessorExtension preprocessorExtension;
public override PreprocessorExtension PreprocessorExtension
{
get
{
if (this.preprocessorExtension == null)
{
this.preprocessorExtension = new MaterialiserPreprocessorExtension();
}
return this.preprocessorExtension;
}
}
}
public class MaterialiserPreprocessorExtension : PreprocessorExtension
{
private static string[] prefixes = { "versionUtil" };
public override string[] Prefixes { get { return prefixes; } }
public override string EvaluateFunction(string prefix, string function, string[] args)
{
string result = null;
switch (prefix)
{
case "versionUtil":
switch (function)
{
case "ToString":
if (0 < args.Length)
{
result = Version.Parse(args[0]).ToString(args.Length >1 && args[1] != null ? int.Parse(args[1]) : 4);
}
else
{
result = String.Empty;
}
break;
}
break;
}
return result;
}
}
One thing that was not immediately obvious - in my Visual Studio Wix project, I had to add
-ext "$(SolutionDir)Materialiser.Wix\bin\Release\Materialiser.Wix.dll" to Properties (Alt+Enter) > Tool Settings > Compiler (candle) and the same in Linker (light).
To use, it is very simple:
in my .wxs file I define a property like this:
<?define ProductVersion="4.6.0.5" ?>
Then I use it in Product like this:
<Product Version="$(var.ProductVersion)" .. >
And then I create anew property like this:
<Property Id="VersionWithNoBuildNumber" Value="$(versionUtil.ToString([ProductVersion], 3))" />
and in my .wxl file I use it normally:
<String Id="WelcomeDlgTitle">{\WixUI_Font_Bigger}Welcome to the [ProductName] v[VersionWithNoBuildNumber] Setup Wizard</String>
I hope this helps someone else too :p