Thursday, December 14, 2017

Problem with terminated site workflows which continue working in Sharepoint Online

Recently we faced with interesting issue in Sharepoint Online: there were several 2013 site workflows on different sub sites which worked this way:

  1. Iterate through publishing pages in Pages doclib of the current site and if current date is close to page’s Valid to date, workflow sends reminder email to responsible person
  2. Wait 1 day and then repeat iteration through pages

i.e. by itself workflow never ends. However it is possible to terminate workflow from UI: Site contents > Site workflows > click workflow > End workflow (see my previous post Restart site workflows in Sharepoint Online)

We published new version from Sharepoint Designer, ended previous workflow instances from UI and started new instances. And here we found interesting issue: terminated workflow instances continued to work, i.e. they continued to send emails of previous version to the users. In UI their status was displayed as Terminated. At the moment we contacted MS support but if you know about this issue please share it in comments.

Monday, December 11, 2017

Install SPFx React Script editor web part as tenant-scoped solution into Sharepoint Online

How it is said in the following MS article Tenant-Scoped solution deployment for SharePoint Framework solutions, it is possible to make SPFx solution tenant scoped. According to documentation components installed with such solution will become immediately available cross the tenant after solution package will be installed to tenant app catalog. Let’s check few internal details of tenant-scoped solutions using Script editor web part for modern pages built in React as example.

Before to start we will need to install the following prerequisites:

  • git for Windows
  • nodejs for Windows
  • also I usually need to globally install gulp
    npm install -g gulp
    and add resolved folder %appdata%\npm to PATH environment variable

So first of all we need to clone git repository with web part code:

git clone https://github.com/SharePoint/sp-dev-fx-webparts.git

After that go to ~/sp-dev-fx-webparts\samples\react-script-editor and run the following commands:

npm install

which will install necessary dependencies into node_modules subfolder (there are more that 1600 dependencies so it will take some time). After that edit config/package-solution.json file: add "skipFeatureDeployment": true there:

it will make our solution tenant-scoped. Also edit config\write-manifest.json file: set cdnBasePath variable to your CDN path, e.g.

https:<tenant>.sharepoint.com/sites/CDN/SiteAssets/SPFx/react-script-editor

After that run the following commands:

gulp --ship

gulp package-solution --ship

First one should create temp folder. You need go to its deploy subfolder and copy files from there to specified CDN. Second command will create sharepoint/solution folder with pzl-script-editor.sppkg file. This file is zip file so if we will unzip it and check AppManifest.xml we will find that it has SkipFeatureDeployment=”true” attribute:

Now deploy to App catalog and enable "Make this solution available to all sites in the organization" option:

After that solution should be available on all sites of your tenant.

Thursday, December 7, 2017

Restart site workflows in Sharepoint Online

Once you have published or made changes in existing site workflow in Sharepoint Online site you will need to restart existing running workflow instances in order to get new changes into use (otherwise they won’t take effect). In order to restart existing site workflow instance in Sharepoint Online you may use the following steps. Go to Site settings > Site contents and click “Site workflows” link on the top of the page:

Opened page will show list of all available site workflows together with list of running workflows and list of completed workflows:

If you will click on some workflow name from the top list – it will be started. If you will click on some running workflow instance workflow details page will be opened:

On this page there is a link “End workflow” (it looks like regular text, not as a link). By clicking this link you may stop existing workflow instance, then return to previous page and start new instance which will use new published workflow version.

Thursday, November 30, 2017

AnonymousPermMask64 for different anonymous settings for Sharepoint lists and document libraries

In addition to changing anonymous settings for the Sharepoint sites (see AllowAnonymousAccess, AnonymousState and AnonymousPermMask64 properties for Sharepoint sites with different anonymous configurations) it may be needed to enable anonymous access for particular lists and doclibs. In this case you need to set Anonymous users can access: Lists and libraries for the parent web (see above link) and enable anonymous access for the list/doclib. It is done from List settings > Permissions for this document library > Anonymous Access. Here you may check only View items permissions for anonymous users on regular web sites with read anonymous access. Let’s see how SPList.AnonymousPermMask64 property will be changed.

1. No anonymous access

In this case SPList.AnonymousPermMask64 = EmptyMask

2. View Items

Now SPList.AnonymousPermMask64 = ViewListItems, OpenItems, ViewVersions, ViewFormPages, Open, UseClientIntegration.

AllowAnonymousAccess, AnonymousState and AnonymousPermMask64 properties for Sharepoint sites with different anonymous configurations

In Sharepoint it is possible to use several anonymous configurations for the sites. They may be changed from Site settings > Site permissions > Anonymous access:

  • Entire Web site
  • Lists and libraries
  • Nothing

Here what description says about them:

Specify what parts of your Web site (if any) anonymous users can access. If you select Entire Web site, anonymous users will be able to view all pages in your Web site and view all lists and items which inherit permissions from the Web site. If you select Lists and libraries, anonymous users will be able to view and change items only for those lists and libraries that have enabled permissions for anonymous users.

And Nothing means that site doesn’t have anonymous access. Depending on used setting Sharepoint changes 3 properties of corresponding SPWeb object: AllowAnonymousAccess, AnonymousState and AnonymousPermMask64. Let’s see how they are changed depending on selection:

1. Anonymous users can access: Entire Web site

Property Value
AllowAnonymousAccess True
AnonymousState On
AnonymousPermMask64 ViewListItems, ViewVersions, ViewFormPages, Open, ViewPages, UseClientIntegration

2. Anonymous users can access: Lists and libraries

Property Value
AllowAnonymousAccess False
AnonymousState Enabled
AnonymousPermMask64 Open

2. Anonymous users can access: Nothing

Property Value
AllowAnonymousAccess False
AnonymousState Disabled
AnonymousPermMask64 EmptyMask

Monday, November 27, 2017

Problem with slow setting of composed look for Sharepoint site via OfficeDevPnP SetComposedLookByUrl method

In order to set composed look for Sharepoint site we may use SetComposedLookByUrl extension method from OfficeDevPnP library. According to documentation it does the following:

Retrieves the named composed look, overrides with specified palette, font, background and master page, and then recursively sets the specified values.

Here is the code of this method:

   1: public static void SetComposedLookByUrl(this Web web, string lookName,
   2:     string paletteServerRelativeUrl = null, string fontServerRelativeUrl = null,
   3:     string backgroundServerRelativeUrl = null, string masterServerRelativeUrl = null,
   4:     bool resetSubsitesToInherit = false, bool updateRootOnly = true)
   5: {
   6:     var paletteUrl = default(string);
   7:     var fontUrl = default(string);
   8:     var backgroundUrl = default(string);
   9:     var masterUrl = default(string);
  10:  
  11:     if (!string.IsNullOrWhiteSpace(lookName))
  12:     {
  13:         var composedLooksList = web.GetCatalog((int)ListTemplateType.DesignCatalog);
  14:  
  15:         // Check for existing, by name
  16:         CamlQuery query = new CamlQuery();
  17:         query.ViewXml = string.Format(CAML_QUERY_FIND_BY_FILENAME, lookName);
  18:         var existingCollection = composedLooksList.GetItems(query);
  19:         web.Context.Load(existingCollection);
  20:         web.Context.ExecuteQueryRetry();
  21:         var item = existingCollection.FirstOrDefault();
  22:  
  23:         if (item != null)
  24:         {
  25:             var lookPaletteUrl = item["ThemeUrl"] as FieldUrlValue;
  26:             if (lookPaletteUrl != null)
  27:             {
  28:                 paletteUrl = new Uri(lookPaletteUrl.Url).AbsolutePath;
  29:             }
  30:             var lookFontUrl = item["FontSchemeUrl"] as FieldUrlValue;
  31:             if (lookFontUrl != null)
  32:             {
  33:                 fontUrl = new Uri(lookFontUrl.Url).AbsolutePath;
  34:             }
  35:             var lookBackgroundUrl = item["ImageUrl"] as FieldUrlValue;
  36:             if (lookBackgroundUrl != null)
  37:             {
  38:                 backgroundUrl = new Uri(lookBackgroundUrl.Url).AbsolutePath;
  39:             }
  40:             var lookMasterUrl = item["MasterPageUrl"] as FieldUrlValue;
  41:             if (lookMasterUrl != null)
  42:             {
  43:                 masterUrl = new Uri(lookMasterUrl.Url).AbsolutePath;
  44:             }
  45:         }
  46:         else
  47:         {
  48:             Log.Error(Constants.LOGGING_SOURCE,
  49: CoreResources.BrandingExtension_ComposedLookMissing, lookName);
  50:             throw new Exception($"Composed look '{lookName}' can not be found; pass " +
  51: "null or empty to set look directly (not based on an existing entry)");
  52:         }
  53:     }
  54:  
  55:     if (!string.IsNullOrEmpty(paletteServerRelativeUrl))
  56:     {
  57:         paletteUrl = paletteServerRelativeUrl;
  58:     }
  59:     if (!string.IsNullOrEmpty(fontServerRelativeUrl))
  60:     {
  61:         fontUrl = fontServerRelativeUrl;
  62:     }
  63:     if (!string.IsNullOrEmpty(backgroundServerRelativeUrl))
  64:     {
  65:         backgroundUrl = backgroundServerRelativeUrl;
  66:     }
  67:     if (!string.IsNullOrEmpty(masterServerRelativeUrl))
  68:     {
  69:         masterUrl = masterServerRelativeUrl;
  70:     }
  71:  
  72:     //URL decode retrieved url's
  73:     paletteUrl = System.Net.WebUtility.UrlDecode(paletteUrl);
  74:     fontUrl = System.Net.WebUtility.UrlDecode(fontUrl);
  75:     backgroundUrl = System.Net.WebUtility.UrlDecode(backgroundUrl);
  76:     masterUrl = System.Net.WebUtility.UrlDecode(masterUrl);
  77:  
  78:     web.SetMasterPageByUrl(masterUrl, resetSubsitesToInherit, updateRootOnly);
  79:     web.SetCustomMasterPageByUrl(masterUrl, resetSubsitesToInherit, updateRootOnly);
  80:     web.SetThemeByUrl(paletteUrl, fontUrl, backgroundUrl, resetSubsitesToInherit,
  81:         updateRootOnly);
  82:  
  83:     // Update/create the "Current" reference in the composed looks gallery
  84:     string currentLookName = GetLocalizedCurrentValue(web);
  85:     web.CreateComposedLookByUrl(currentLookName, paletteUrl, fontUrl, backgroundUrl,
  86:         masterUrl, displayOrder: 0);
  87: }

The problem is that on the real production environments this method may work very slow (we needed to set composed look on all sub sites, i.e. pass resetSubsitesToInherit = true and updateRootOnly = false). When apply it on the site with many sub sites (about 4000 sub site) it worked almost 15 hours and then was interrupted with network connectivity exception.

Another problematic moment is that method doesn’t show any progress indicator, i.e. you just have to wait watching black screen all that time. At the moment I see only 1 workaround which at least will allow to see progress of the work: iterate through sub sites in own external loop, print url of currently updated web and call SetComposedLookByUrl for each web site separately with resetSubsitesToInherit = false and updateRootOnly = true. If you know other solutions please share them in comments.

Sunday, November 26, 2017

Real time currency conversion using web service for .Net

Sometimes you need to automatically convert currencies in the app. In order to do that you need to get current conversion ratios for appropriate currencies. There are many web services which provide this data: free and chargeable. In this post I will use free web service provided by Central bank of Russia, in your code you may use other services, e.g. service provided by Central European bank. There is convenient web service which returns currencies conversion ratios for the current day:

http://www.cbr.ru/scripts/XML_daily.asp?date_req=dd/MM/yyyy

E.g. if we want to get it for 26 Nov 2017 we need to use the following url:

http://www.cbr.ru/scripts/XML_daily.asp?date_req=26/11/2017

Here is result:

   1: <?xml version="1.0" encoding="windows-1251"?>
   2: <ValCurs Date="25.11.2017" name="Foreign Currency Market">
   3: <Valute ID="R01010">
   4:     <NumCode>036</NumCode>
   5:     <CharCode>AUD</CharCode>
   6:     <Nominal>1</Nominal>
   7:     <Name>Австралийский доллар</Name>
   8:     <Value>44,5778</Value>
   9: </Valute>
  10: <Valute ID="R01020A">
  11:     <NumCode>944</NumCode>
  12:     <CharCode>AZN</CharCode>
  13:     <Nominal>1</Nominal>
  14:     <Name>Азербайджанский манат</Name>
  15:     <Value>34,4609</Value>
  16: </Valute>
  17: <Valute ID="R01035">
  18:     <NumCode>826</NumCode>
  19:     <CharCode>GBP</CharCode>
  20:     <Nominal>1</Nominal>
  21:     <Name>Фунт стерлингов Соединенного королевства</Name>
  22:     <Value>77,9644</Value>
  23: </Valute>
  24: <Valute ID="R01060">
  25:     <NumCode>051</NumCode>
  26:     <CharCode>AMD</CharCode>
  27:     <Nominal>100</Nominal>
  28:     <Name>Армянских драмов</Name>
  29:     <Value>12,0933</Value>
  30: </Valute>
  31: <Valute ID="R01090B">
  32:     <NumCode>933</NumCode>
  33:     <CharCode>BYN</CharCode>
  34:     <Nominal>1</Nominal>
  35:     <Name>Белорусский рубль</Name>
  36:     <Value>29,2835</Value>
  37: </Valute>
  38: <Valute ID="R01100">
  39:     <NumCode>975</NumCode>
  40:     <CharCode>BGN</CharCode>
  41:     <Nominal>1</Nominal>
  42:     <Name>Болгарский лев</Name>
  43:     <Value>35,4717</Value>
  44: </Valute>
  45: <Valute ID="R01115">
  46:     <NumCode>986</NumCode>
  47:     <CharCode>BRL</CharCode>
  48:     <Nominal>1</Nominal>
  49:     <Name>Бразильский реал</Name>
  50:     <Value>18,1669</Value>
  51: </Valute>
  52: <Valute ID="R01135">
  53:     <NumCode>348</NumCode>
  54:     <CharCode>HUF</CharCode>
  55:     <Nominal>100</Nominal>
  56:     <Name>Венгерских форинтов</Name>
  57:     <Value>22,2428</Value>
  58: </Valute>
  59: <Valute ID="R01200">
  60:     <NumCode>344</NumCode>
  61:     <CharCode>HKD</CharCode>
  62:     <Nominal>10</Nominal>
  63:     <Name>Гонконгских долларов</Name>
  64:     <Value>74,9648</Value>
  65: </Valute>
  66: <Valute ID="R01215">
  67:     <NumCode>208</NumCode>
  68:     <CharCode>DKK</CharCode>
  69:     <Nominal>10</Nominal>
  70:     <Name>Датских крон</Name>
  71:     <Value>93,2302</Value>
  72: </Valute>
  73: <Valute ID="R01235">
  74:     <NumCode>840</NumCode>
  75:     <CharCode>USD</CharCode>
  76:     <Nominal>1</Nominal>
  77:     <Name>Доллар США</Name>
  78:     <Value>58,5318</Value>
  79: </Valute>
  80: <Valute ID="R01239">
  81:     <NumCode>978</NumCode>
  82:     <CharCode>EUR</CharCode>
  83:     <Nominal>1</Nominal>
  84:     <Name>Евро</Name>
  85:     <Value>69,3309</Value>
  86: </Valute>
  87: <Valute ID="R01270">
  88:     <NumCode>356</NumCode>
  89:     <CharCode>INR</CharCode>
  90:     <Nominal>100</Nominal>
  91:     <Name>Индийских рупий</Name>
  92:     <Value>90,5539</Value>
  93: </Valute>
  94: <Valute ID="R01335">
  95:     <NumCode>398</NumCode>
  96:     <CharCode>KZT</CharCode>
  97:     <Nominal>100</Nominal>
  98:     <Name>Казахстанских тенге</Name>
  99:     <Value>17,7114</Value>
 100: </Valute>
 101: <Valute ID="R01350">
 102:     <NumCode>124</NumCode>
 103:     <CharCode>CAD</CharCode>
 104:     <Nominal>1</Nominal>
 105:     <Name>Канадский доллар</Name>
 106:     <Value>45,9975</Value>
 107: </Valute>
 108: <Valute ID="R01370">
 109:     <NumCode>417</NumCode>
 110:     <CharCode>KGS</CharCode>
 111:     <Nominal>100</Nominal>
 112:     <Name>Киргизских сомов</Name>
 113:     <Value>83,9194</Value>
 114: </Valute>
 115: <Valute ID="R01375">
 116:     <NumCode>156</NumCode>
 117:     <CharCode>CNY</CharCode>
 118:     <Nominal>10</Nominal>
 119:     <Name>Китайских юаней</Name>
 120:     <Value>88,6349</Value>
 121: </Valute>
 122: <Valute ID="R01500">
 123:     <NumCode>498</NumCode>
 124:     <CharCode>MDL</CharCode>
 125:     <Nominal>10</Nominal>
 126:     <Name>Молдавских леев</Name>
 127:     <Value>33,7359</Value>
 128: </Valute>
 129: <Valute ID="R01535">
 130:     <NumCode>578</NumCode>
 131:     <CharCode>NOK</CharCode>
 132:     <Nominal>10</Nominal>
 133:     <Name>Норвежских крон</Name>
 134:     <Value>71,9099</Value>
 135: </Valute>
 136: <Valute ID="R01565">
 137:     <NumCode>985</NumCode>
 138:     <CharCode>PLN</CharCode>
 139:     <Nominal>1</Nominal>
 140:     <Name>Польский злотый</Name>
 141:     <Value>16,4902</Value>
 142: </Valute>
 143: <Valute ID="R01585F">
 144:     <NumCode>946</NumCode>
 145:     <CharCode>RON</CharCode>
 146:     <Nominal>1</Nominal>
 147:     <Name>Румынский лей</Name>
 148:     <Value>14,9107</Value>
 149: </Valute>
 150: <Valute ID="R01589">
 151:     <NumCode>960</NumCode>
 152:     <CharCode>XDR</CharCode>
 153:     <Nominal>1</Nominal>
 154:     <Name>СДР (специальные права заимствования)</Name>
 155:     <Value>82,5796</Value>
 156: </Valute>
 157: <Valute ID="R01625">
 158:     <NumCode>702</NumCode>
 159:     <CharCode>SGD</CharCode>
 160:     <Nominal>1</Nominal>
 161:     <Name>Сингапурский доллар</Name>
 162:     <Value>43,4502</Value>
 163: </Valute>
 164: <Valute ID="R01670">
 165:     <NumCode>972</NumCode>
 166:     <CharCode>TJS</CharCode>
 167:     <Nominal>10</Nominal>
 168:     <Name>Таджикских сомони</Name>
 169:     <Value>66,3656</Value>
 170: </Valute>
 171: <Valute ID="R01700J">
 172:     <NumCode>949</NumCode>
 173:     <CharCode>TRY</CharCode>
 174:     <Nominal>1</Nominal>
 175:     <Name>Турецкая лира</Name>
 176:     <Value>14,8728</Value>
 177: </Valute>
 178: <Valute ID="R01710A">
 179:     <NumCode>934</NumCode>
 180:     <CharCode>TMT</CharCode>
 181:     <Nominal>1</Nominal>
 182:     <Name>Новый туркменский манат</Name>
 183:     <Value>16,7243</Value>
 184: </Valute>
 185: <Valute ID="R01717">
 186:     <NumCode>860</NumCode>
 187:     <CharCode>UZS</CharCode>
 188:     <Nominal>10000</Nominal>
 189:     <Name>Узбекских сумов</Name>
 190:     <Value>72,3946</Value>
 191: </Valute>
 192: <Valute ID="R01720">
 193:     <NumCode>980</NumCode>
 194:     <CharCode>UAH</CharCode>
 195:     <Nominal>10</Nominal>
 196:     <Name>Украинских гривен</Name>
 197:     <Value>21,7429</Value>
 198: </Valute>
 199: <Valute ID="R01760">
 200:     <NumCode>203</NumCode>
 201:     <CharCode>CZK</CharCode>
 202:     <Nominal>10</Nominal>
 203:     <Name>Чешских крон</Name>
 204:     <Value>27,2844</Value>
 205: </Valute>
 206: <Valute ID="R01770">
 207:     <NumCode>752</NumCode>
 208:     <CharCode>SEK</CharCode>
 209:     <Nominal>10</Nominal>
 210:     <Name>Шведских крон</Name>
 211:     <Value>70,4829</Value>
 212: </Valute>
 213: <Valute ID="R01775">
 214:     <NumCode>756</NumCode>
 215:     <CharCode>CHF</CharCode>
 216:     <Nominal>1</Nominal>
 217:     <Name>Швейцарский франк</Name>
 218:     <Value>59,6350</Value>
 219: </Valute>
 220: <Valute ID="R01810">
 221:     <NumCode>710</NumCode>
 222:     <CharCode>ZAR</CharCode>
 223:     <Nominal>10</Nominal>
 224:     <Name>Южноафриканских рэндов</Name>
 225:     <Value>41,9715</Value>
 226: </Valute>
 227: <Valute ID="R01815">
 228:     <NumCode>410</NumCode>
 229:     <CharCode>KRW</CharCode>
 230:     <Nominal>1000</Nominal>
 231:     <Name>Вон Республики Корея</Name>
 232:     <Value>53,9061</Value>
 233: </Valute>
 234: <Valute ID="R01820">
 235:     <NumCode>392</NumCode>
 236:     <CharCode>JPY</CharCode>
 237:     <Nominal>100</Nominal>
 238:     <Name>Японских иен</Name>
 239:     <Value>52,5538</Value>
 240: </Valute>
 241: </ValCurs>

It shows conversion ratios related with Russian Ruble, e.g. if we check EUR:

   1: <Valute ID="R01239">
   2:     <NumCode>978</NumCode>
   3:     <CharCode>EUR</CharCode>
   4:     <Nominal>1</Nominal>
   5:     <Name>Евро</Name>
   6:     <Value>69,3309</Value>
   7: </Valute>

it shows that 1 (Nominal) Euro is equal to 69,3309 (Value) Rub. Having conversion ratios for all currencies and Ruble it is possible to get conversion ratios between any 2 currencies using simple proportions math. The following code shows how to convert value between RUR, EUR and USD in any order and direction:

   1:  
   2: public decimal? Convert(decimal sourceValue, Currency sourceCurrency, Currency targetCurrency)
   3: {
   4:     try
   5:     {
   6:         if (sourceCurrency == targetCurrency)
   7:         {
   8:             return sourceValue;
   9:         }
  10:  
  11:         var now = DateTime.Now;
  12:         string url = string.Format("http://www.cbr.ru/scripts/XML_daily.asp?date_req={0}",
  13:             now.ToString("dd/MM/yyyy"));
  14:         string str = this.webClient.Download(url, Encoding.GetEncoding("windows-1251"));
  15:         if (string.IsNullOrEmpty(str))
  16:         {
  17:             return null;
  18:         }
  19:         var doc = XDocument.Parse(str);
  20:         var elEUR = doc.Root.Elements("Valute").FirstOrDefault(e =>
  21:             e.Elements("CharCode").ElementAt(0).Value == "EUR");
  22:         decimal? nominalEUR =
  23:             DecimalHelper.ParseWithDotSeparator(elEUR.Element("Nominal").Value);
  24:         decimal? valueEUR =
  25:             DecimalHelper.ParseWithDotSeparator(elEUR.Element("Value").Value);
  26:  
  27:         var elUSD = doc.Root.Elements("Valute").FirstOrDefault(e =>
  28:             e.Elements("CharCode").ElementAt(0).Value == "USD");
  29:         decimal? nominalUSD =
  30:             DecimalHelper.ParseWithDotSeparator(elUSD.Element("Nominal").Value);
  31:         decimal? valueUSD =
  32:             DecimalHelper.ParseWithDotSeparator(elUSD.Element("Value").Value);
  33:  
  34:         double koefFromEURtoRUB = (double)valueEUR.Value / (double)nominalEUR.Value;
  35:         double koefFromUSDtoRUB = (double)valueUSD.Value / (double)nominalUSD.Value;
  36:  
  37:         double koefFromRUBtoEUR = 1 / koefFromEURtoRUB;
  38:         double koefFromUSDtoEUR = koefFromUSDtoRUB / koefFromEURtoRUB;
  39:  
  40:         double koefFromRUBtoUSD = 1 / koefFromUSDtoRUB;
  41:         double koefFromEURtoUSD = koefFromEURtoRUB / koefFromUSDtoRUB;
  42:  
  43:         double? koeff = null;
  44:         if (sourceCurrency == Currency.Rub)
  45:         {
  46:             if (targetCurrency == Currency.Eur)
  47:             {
  48:                 koeff = koefFromRUBtoEUR;
  49:             }
  50:             else if (targetCurrency == Currency.Usd)
  51:             {
  52:                 koeff = koefFromRUBtoUSD;
  53:             }
  54:         }
  55:         else if (sourceCurrency == Currency.Eur)
  56:         {
  57:             if (targetCurrency == Currency.Rub)
  58:             {
  59:                 koeff = koefFromEURtoRUB;
  60:             }
  61:             else if (targetCurrency == Currency.Usd)
  62:             {
  63:                 koeff = koefFromEURtoUSD;
  64:             }
  65:         }
  66:         else if (sourceCurrency == Currency.Usd)
  67:         {
  68:             if (targetCurrency == Currency.Rub)
  69:             {
  70:                 koeff = koefFromUSDtoRUB;
  71:             }
  72:             else if (targetCurrency == Currency.Eur)
  73:             {
  74:                 koeff = koefFromUSDtoEUR;
  75:             }
  76:         }
  77:         if (koeff == null)
  78:         {
  79:             return null;
  80:         }
  81:  
  82:         var targetValue = (decimal)Math.Round(((double)sourceValue) * koeff.Value, 2);
  83:         return targetValue;
  84:     }
  85:     catch (Exception x)
  86:     {
  87:         return null;
  88:     }
  89: }

Helper classes WebClient and DecimalHelper look like this:

   1: public class WebClient
   2: {
   3:     public string Download(string url, Encoding encoding)
   4:     {
   5:         try
   6:         {
   7:             if (string.IsNullOrEmpty(url))
   8:             {
   9:                 return string.Empty;
  10:             }
  11:             byte[] data;
  12:             using (var webClient = new System.Net.WebClient())
  13:             {
  14:                 data = webClient.DownloadData(url);
  15:             }
  16:             if (data == null)
  17:             {
  18:                 return string.Empty;
  19:             }
  20:             return encoding.GetString(data);
  21:         }
  22:         catch (Exception x)
  23:         {
  24:             return string.Empty;
  25:         }
  26:     }
  27: }

and

   1: public class DecimalHelper
   2: {
   3:     public static decimal? ParseWithDotSeparator(string val)
   4:     {
   5:         if (string.IsNullOrEmpty(val))
   6:         {
   7:             return null;
   8:         }
   9:         return decimal.Parse(val.Replace(",", "."), CultureInfo.InvariantCulture);
  10:     }
  11: }

Convert method receives 3 parameters:

  1. source value
  2. source currency
  3. target currency

At first we download current ratios from web service (lines 10-17). Then we calculate ratios for all required conversions EUR to RUR, USD to RUR, RUR to EUR, USD to EUR, RUR to USD, EUR to USD (lines 35-42) and choice correct ratio depending on arguments. If you will compare result with one get from Google (try to google e.g. eur to usd – Google will show conversion calculator), you will see that it is almost the same. Of course if conversion accuracy is crucial for your app then you need to use another web service which is updated more frequently.