?. ve ?[ Operatörleri

Visual Studio 2015 ile gelen (?.) ve (?[) operatörleri ile üyeye erişim (?.) veya index operasyonu (?[) öncesinde null testi yapabilmekteyiz.

int? length = customers?.Length; // customers null ise null değilse customers.Length
Customer first = customers?[0];  // customers null ise null değilse customers[0]
int? count = customers?[0]?.Orders?.Count();  // customers, customers[0], customers[0].Orders null ise null değilse customers[0].Orders.Count()

TransactionScope ile Transaction Yönetimi

Transaction yönetimi iş uygulamalarında çok önemlidir. .NET’te System.Transactions altında TransactionScope sınıfı ile transaction yönetimi kolay ve güvenli bir şekilde yapılabilmektedir.

  • Console Application oluşturulur.
  • Projede References > Add Reference… > Assemblies > Framework > System.Transactions seçilir ve eklenir.
  • Projeye aşağıdaki Class eklenir.
    MoneyPayTransaction.cs

    using System;
    using System.Transactions;
    
    namespace TransactionTest
    {
        public class MoneyPayTransaction : IEnlistmentNotification
        {
            public MoneyPayTransaction()
            {
                // İşlem transaction olarak kaydedilir.
                Transaction.Current.EnlistVolatile(this, EnlistmentOptions.None);
            }
    
            public void Commit(Enlistment enlistment)
            {
                Console.WriteLine("Commit.");
    
                // İşlem onaylanır.
    
                enlistment.Done();
            }
    
            public void InDoubt(Enlistment enlistment)
            {
                Console.WriteLine("InDoubt.");
    
                // İşlem bilinmez bir durumda kalır.
    
                enlistment.Done();
            }
    
            public void Prepare(PreparingEnlistment preparingEnlistment)
            {
                Console.WriteLine("Prepare.");
    
                // İşlem kaydı hazırlığı yapılır.
    
                preparingEnlistment.Prepared();
            }
    
            public void Rollback(Enlistment enlistment)
            {
                Console.WriteLine("Rollback.");
    
                // İşlem geri sarılır.
    
                enlistment.Done();
            }
        }
    }
    
  • Program.cs sınıfı aşağıdaki gibi değiştirilir.
    using System;
    using System.Transactions;
    
    namespace TransactionTest
    {
        class Program
        {
            static void Main(string[] args)
            {
                try
                {
                    Test();
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
    
                Console.ReadKey();
            }
    
            static void Test()
            {
                using (TransactionScope ts = new TransactionScope())
                {
                    var mpt = new MoneyPayTransaction();
    
                    // İşlem yapılır.
    
                    while (true)
                    {
                        Console.Write("Transaction başarılı olsun mu? [Y|N]");
                        var c = Console.ReadKey();
                        Console.WriteLine();
    
                        if ((c.KeyChar == 'Y') || (c.KeyChar == 'y'))
                        {
                            ts.Complete();
                            break;
                        }
                        else if ((c.KeyChar == 'N') || (c.KeyChar == 'n'))
                        {
                            break;
                        }
                    }
                }
            }
        }
    }
    

TransactionScope çalışma mantığı şu şekildedir:

  • Eğer Complete çağrılmış ise işlemi tamamlamak için MoneyPayTransaction sınıfından Prepare ve Commit metotları otomatik olarak çağrılır.
  • Eğer Complete çağrılmamış ve TransactionScope dışına çıkılmış ise işlemi geriye sarmak için MoneyPayTransaction sınıfından Rollback metodu otomatik olarak çağrılır.

Attached Dependency Property ile Resimli Tuş Oluşturma

Yeni bir kontrol oluşturmadan, mevcut Button kontrolüne ImageSource ve Text ekleyerek resimli tuş oluşturabiliyoruz. Ek olarak resim yüklenirken durumunu tuşun üzerinde görüntülemek için ShowProgress ve ProgressValue ekledim.
Buradan örnek projeyi indirebilirsiniz.

<Window x:Class="LazyImageButton.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ap="clr-namespace:LazyImageButton.AttachedProperties"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded" DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <Button Style="{StaticResource SolidImageButtonStyle}" ap:ImageButton.ImageSource="{Binding Source}" ap:ImageButton.Text="Text" ap:ImageButton.ShowProgress="True" ap:ImageButton.ProgressValue="{Binding ProgressValue}"/>
    </Grid>
</Window>

ASP.NET MVC Web API ile Dosya Sunucusu

Örnek projeyi buradan indirebilirsiniz.

  • Asp.Net Web Api projesi oluşturulur.
  • Dosyalar App_Data klasörü altına atılır.
  • Web.Config’de  system.webServer altına aşağıdaki satır eklenir.
    <modules runAllManagedModulesForAllRequests="true"/>
  • Controllers altında FileController.cs oluşturulur.
    using System.IO;
    using System.Net;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Web;
    using System.Web.Http;
    
    namespace FileServer.Controllers
    {
        public class FilesController : ApiController
        {
            // GET api/files/test_v1.jpg?sessionId=test
            public HttpResponseMessage GetFile(string sessionId, string name)
            {
                HttpResponseMessage response;
    
                // Session kontrolü burada yapılabilir.
                if (sessionId == "test")
                {
                    var filePath = HttpContext.Current.Server.MapPath("~/App_Data/" + name);
    
                    if (File.Exists(filePath))
                    {
                        response = new HttpResponseMessage 
                                       { 
                                           StatusCode = HttpStatusCode.OK, 
                                           Content = new StreamContent(File.OpenRead(filePath)) 
                                       };
    
                        response.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(Path.GetExtension(filePath)));
    
                        return response;
                    }
                    else
                    {
                        response = new HttpResponseMessage(HttpStatusCode.NotFound);
                    }
                }
                else
                {
                    response = new HttpResponseMessage(HttpStatusCode.Forbidden);
                }
    
                return response;
            }
        }
    }
    

Programlar Arası İletişim

Windows işletim sistemi, uygulamalar arasında iletişimi ve veri paylaşımını sağlamak için çeşitli mekanizmalara sahiptir. Bu mekanizmalara işlemler arası iletişim – Interprocess Communications (IPC) adı verilir. Tipik olarak uygulamalar IPC’yi sunucu ve istemci olarak kategorize ederek kullanırlar.

Windows’ta aşağıdaki IPC mekanizmaları mevcuttur:

  • Pano (Clipboard)
  • Bileşen Nesne Modeli (COM)
  • Veri Kopyalama (Data Copy)
  • Dinamik Veri Takası (DDE)
  • Dosya Adresleme (File Mapping)
  • İleti Yuvaları (Mailslots)
  • Borular (Pipes)
  • Uzak Yordam Çağrısı (RPC)
  • Windows Soketleri (Windows Sockets)

Ben bunların arasından otomatik veri dönüştürme ve farklı işletim sistemleri ile çalışabilme özelliğinden dolayı RPC’yi tercih ediyorum. RPC ile yüksek performanslı dağıtık uygulamalar geliştirilebilinir. RPC kütüphanesi olarak da Zyan Communication Framework‘ü tercih ediyorum.

Örnek bir sohbet uygulaması oluşturdum. Host çalıştığı anda bağlantıya başlıyor ve 3 adet farklı client çalıştırıyor. Client’lar birbirlerine mesaj gönderebiliyor.

Buradan oluşturduğum örnek projeyi indirebilirsiniz.

REST Servis Çağrısı

REST GET/POST/PUT/DELETE yapmak için uyguladığım adımlar şu şekilde:

  • Visual Studio’da Visual C#/Windows Desktop/Console Application projesi oluşturdum. Adını RestClientSample koydum.
  • NuGet üzerinden Microsoft.AspNet.WebApi.Client paketini yükledim.
  • Rest servise gönderilecek ve servisten dönecek nesnelere karşılık gelen sınıfları oluşturdum.
  • Program.cs’yi aşağıdaki gibi değiştirdim.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace RestClientSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                RunAsync().Wait();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.StackTrace);
            }

            Console.ReadKey();
        }

        static async Task RunAsync()
        {
            using (var client = new HttpClient())
            {
                // Servis adresi
                client.BaseAddress = new Uri("http://webadresi.com/");
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                // Servis Basic Http Authentication istiyorsa eklenir.
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(new UTF8Encoding().GetBytes(string.Format("{0}:{1}", "TestUser", "Pass"))));

                // HTTP GET
                HttpResponseMessage response = await client.GetAsync("api/IotDatas/1");
                if (response.IsSuccessStatusCode)
                {
                    IotData product = await response.Content.ReadAsAsync<IotData>();
                    Console.WriteLine("{0}\t${1}\t{2}", product.Name, product.Value, product.CreateDate);
                }

                // HTTP POST
                var iotData = new IotData { Name = "Test", Value = null, CreateDate = DateTime.Now };
                response = await client.PostAsJsonAsync("api/IotDatas", iotData);
                if (response.IsSuccessStatusCode)
                {
                    Uri gizmoUrl = response.Headers.Location;

                    // HTTP PUT
                    iotData.Value = null;
                    response = await client.PutAsJsonAsync(gizmoUrl, iotData);

                    // HTTP DELETE
                    response = await client.DeleteAsync(gizmoUrl);
                }
            }
        }
    }
}

DevExpress MVVM Kütüphanesi ile POCO View Model Oluşturma

Model-View-ViewModel deseni WPF ve Silverlight uygulamaları yazmak için kullanılan en popüler desendir. İş uygulamalarında oldukça yararlı olan birim testleri için esnek bir mimari geliştirmeye olanak sağlar.

Geleneksel MVVM geliştirmede bağlanabilir nesneler ve komutlar için ciddi oranda View Model kodu yazılır. DevExpress MVVM kütüphanesi bu işi oldukça kolaylaştırmış durumda. Bu kütüphane sayesinde POCO sınıflarını kullanarak çok hızlı bir şekilde View Model oluşturabiliyoruz. Ücretsiz olmasına karşın oldukça zengin bir kütüphane.

Projeye GitHub üzerinden ulaşabiliyoruz. Examples klasörü altında bir çok örnek kod da mevcut.
https://github.com/DevExpress/DevExpress.Mvvm.Free

Örnek kullanımını göstermek için bir Visual Studio’da bir örnek WPF projesi oluşturdum ve aşağıdaki adımları uyguladım.

  • Düzenli olması için bu projeye Views, ViewModels ve Models klasörlerini ekledim.
    MainWindow’u Views klasörüne taşıdım.
  • MainWindow.xaml’de MainWindow.cs’de namespace’leri düzenledim.
  • App.xaml’de StartupUri’yi “Views/MainWindow” olarak değiştirdim.
  • NuGet ile “DevExpressMvvm” kütüphanesini ekledim.
  • Models altına “Customer” sınıfını ekledim.
  • ViewModels altına “MainWindowViewModel” sınıfını ekledim.
  • MainWindow.xaml’e aşağıdaki namespace’leri ekledim.
    xmlns:ViewModels="clr-namespace:DxMvvmSample.ViewModels"
    xmlns:dxmvvm="http://schemas.devexpress.com/winfx/2008/xaml/mvvm"
    
  • MainWindow.xaml’de Window’a ViewModel’i aşağıdaki gibi bağladım.
    DataContext="{dxmvvm:ViewModelSource Type=ViewModels:MainWindowViewModel}"
    
  • View Model üzerinden MessageBox gösterebilmek için MainWindow.xaml’e aşağıdaki kodu ekledim.
    <dxmvvm:Interaction.Behaviors>
        <dxmvvm:MessageBoxService/>
    </dxmvvm:Interaction.Behaviors>
    
  • Ekranda basit bir giriş formu oluşturdum. Binding’leri ekledim.
  • PasswordBox’un Password özelliği “Dependency Property” olmadığı için orada “DependencyPropertyBehavior” kullandım.

Sonuç olarak aşağıdaki gibi Model, View ve View Model sınıfları oluştu.

Model:

namespace DxMvvmSample.Models
{
    public class Customer
    {
        public virtual string UserName { get; set; }
    }
}

View:

<Window x:Class="DxMvvmSample.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ViewModels="clr-namespace:DxMvvmSample.ViewModels" xmlns:dxmvvm="http://schemas.devexpress.com/winfx/2008/xaml/mvvm" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:DxMvvmSample" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525" WindowStartupLocation="CenterScreen" DataContext="{dxmvvm:ViewModelSource Type=ViewModels:MainWindowViewModel}">
    <dxmvvm:Interaction.Behaviors>
        <dxmvvm:MessageBoxService/>
    </dxmvvm:Interaction.Behaviors>
    <Grid>
        <Grid Width="200" VerticalAlignment="Center" Background="AliceBlue">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="48"/>
                <RowDefinition Height="48"/>
                <RowDefinition Height="48"/>
            </Grid.RowDefinitions>
            <TextBlock Text="User Name:" VerticalAlignment="Center" Margin="3,0,3,0"/>
            <TextBox Grid.Column="1" Text="{Binding Customer.UserName}" VerticalContentAlignment="Center"/>
            <TextBlock Grid.Row="1" Text="Password:" VerticalAlignment="Center" Margin="3,0,3,0"/>
            <PasswordBox Grid.Column="1" Grid.Row="1" VerticalContentAlignment="Center">
                <dxmvvm:Interaction.Behaviors>
                    <dxmvvm:DependencyPropertyBehavior PropertyName="Password" EventName="PasswordChanged" Binding="{Binding Password, Mode=TwoWay}"/>
                </dxmvvm:Interaction.Behaviors>
            </PasswordBox>
            <Button Grid.ColumnSpan="2" Grid.Row="2" Content="Login" Command="{Binding LoginCommand}"/>
        </Grid>
    </Grid>
</Window>

View Model:

using DevExpress.Mvvm;
using DevExpress.Mvvm.POCO;
using DxMvvmSample.Models;

namespace DxMvvmSample.ViewModels
{
    public class MainWindowViewModel
    {
        protected MainWindowViewModel()
        {
            Customer = ViewModelSource.Create(() => new Customer());
        }

        public static MainWindowViewModel Create()
        {
            return ViewModelSource.Create(() => new MainWindowViewModel());
        }

        protected virtual IMessageBoxService MessageBoxService { get { return null; } }

        public virtual string Password { get; set; }

        public virtual Customer Customer { get; set; }

        public void Login()
        {
            if (CanLogin())
            {
                MessageBoxService.ShowMessage("Welcome " + Customer.UserName + "!");
            }
            else
            {
                MessageBoxService.ShowMessage("Failed.");
            }
        }

        public bool CanLogin()
        {
            return !string.IsNullOrWhiteSpace(Customer.UserName) && !string.IsNullOrEmpty(Password);
        }
    }
}

Buradan oluşturduğum örnek projeyi indirebilirsiniz.

Regex ile Substring

201307020001282007201506112.2.1.9QkZFQkZCR

Yukarıdaki metinde koyu olan sürüm bilgisini PL/SQL’de almak istiyoruz. Kurallar şu şekilde;

  • İlk noktadan önce 1 rakam olur.
  • Diğer noktalardan önce en az 1 rakam olabilir.
  • Son noktadan sonra en az 1 rakam olabilir.
  • Toplamda 3 nokta olabilir.

Sürüm bilgisinin yeri değişken olacağı için işlemi regex ile yapmak istiyoruz. Bunun için aşağıdaki fonksiyonu kullanabiliriz.

REGEXP_SUBSTR('201307020001282007201506112.2.1.9QkZFQkZCR', '\d\.\d+\.\d+\.\d+')

Aynı zamanda istediğimizi metini almak ve istediğimiz formatta göstermek istiyorsak REGEXP_REPLACE kullanabiliriz.

Phone=”1234567890

Yukarıdaki metinden telefon numarasını alıp +90 xxx xxx xx xx formatında göstermek istiyoruz. Bunun için aşağıdaki fonksiyonu kullanabiliriz.

REGEXP_REPLACE('Phone="1234567890"', '^Phone="(.{3})(.{3})(.{2})(.{2})"$', '+90 \1 \2 \3 \4')

Vergi Kimlik Numarası Kontrolü

Aşağıdaki metot ile vergi kimlik numaralarını(VKN) kontrol edebiliriz.

bool IsVknValid(string vkn)
{
    try
    {
        if (vkn.Length == 10)
        {
            var x = new int[9];
            var y = new int[9];

            for (int i = 0; i < 9; i++)
            {
                x[i] = (int.Parse(vkn[i].ToString()) + 9 - i) % 10;

                y[i] = (x[i] * (int)Math.Pow(2, 9 - i)) % 9;

                if (x[i] != 0 && y[i] == 0)
                {
                    y[i] = 9;
                }
            }

            return ((10 - (y.Sum() % 10)) % 10) == int.Parse(vkn[9].ToString());
        }
        else
        {
            return false;
        }
    }
    catch (Exception)
    {
        return false;
    }
}

WPF için ProgressRing

WPF üzerinde Windows 8’dekine benzer aktiviteyi gösteren bir kontrol oluşturabiliriz.

Not: MahApps.Metro kütüphanesi bu kontrolü ve daha bir çok farklı kontrolü içermektedir. Bunun yanında tema desteği de mevcuttur.
http://mahapps.com/

Kullanımı:
ProgressRing.cs dosyasını Controls klasörü altında oluşturulur.
ProgressRing.xaml dosyasını Styles altında oluşturulur.
App.xaml dosyasında Application.Resources içine ProgressRing.xaml resource dictionary olarak eklenir.
Kullanacağımız sayfada namespace tanımlanarak kontrol kullanılabilir.

ProgressRing.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace ProgressRingTest.Controls
{
    [TemplateVisualState(Name = "Large", GroupName = "SizeStates")]
    [TemplateVisualState(Name = "Small", GroupName = "SizeStates")]
    [TemplateVisualState(Name = "Inactive", GroupName = "ActiveStates")]
    [TemplateVisualState(Name = "Active", GroupName = "ActiveStates")]
    public class ProgressRing : Control
    {
        public static readonly DependencyProperty BindableWidthProperty = DependencyProperty.Register("BindableWidth", typeof(double), typeof(ProgressRing), new PropertyMetadata(default(double), BindableWidthCallback));

        public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register("IsActive", typeof(bool), typeof(ProgressRing), new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, IsActiveChanged));

        public static readonly DependencyProperty IsLargeProperty = DependencyProperty.Register("IsLarge", typeof(bool), typeof(ProgressRing), new PropertyMetadata(true, IsLargeChangedCallback));

        public static readonly DependencyProperty MaxSideLengthProperty = DependencyProperty.Register("MaxSideLength", typeof(double), typeof(ProgressRing), new PropertyMetadata(default(double)));

        public static readonly DependencyProperty EllipseDiameterProperty = DependencyProperty.Register("EllipseDiameter", typeof(double), typeof(ProgressRing), new PropertyMetadata(default(double)));

        public static readonly DependencyProperty EllipseOffsetProperty = DependencyProperty.Register("EllipseOffset", typeof(Thickness), typeof(ProgressRing), new PropertyMetadata(default(Thickness)));

        private List<Action> _deferredActions = new List<Action>();

        static ProgressRing()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(ProgressRing), new FrameworkPropertyMetadata(typeof(ProgressRing)));
        }

        public ProgressRing()
        {
            SizeChanged += OnSizeChanged;
        }

        public double MaxSideLength
        {
            get { return (double)GetValue(MaxSideLengthProperty); }
            private set { SetValue(MaxSideLengthProperty, value); }
        }

        public double EllipseDiameter
        {
            get { return (double)GetValue(EllipseDiameterProperty); }
            private set { SetValue(EllipseDiameterProperty, value); }
        }

        public Thickness EllipseOffset
        {
            get { return (Thickness)GetValue(EllipseOffsetProperty); }
            private set { SetValue(EllipseOffsetProperty, value); }
        }

        public double BindableWidth
        {
            get { return (double)GetValue(BindableWidthProperty); }
            private set { SetValue(BindableWidthProperty, value); }
        }

        public bool IsActive
        {
            get { return (bool)GetValue(IsActiveProperty); }
            set { SetValue(IsActiveProperty, value); }
        }

        public bool IsLarge
        {
            get { return (bool)GetValue(IsLargeProperty); }
            set { SetValue(IsLargeProperty, value); }
        }

        private static void BindableWidthCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            var ring = dependencyObject as ProgressRing;
            if (ring == null)
                return;

            var action = new Action(() =>
            {
                ring.SetEllipseDiameter(
                    (double)dependencyPropertyChangedEventArgs.NewValue);
                ring.SetEllipseOffset(
                    (double)dependencyPropertyChangedEventArgs.NewValue);
                ring.SetMaxSideLength(
                    (double)dependencyPropertyChangedEventArgs.NewValue);
            });

            if (ring._deferredActions != null)
                ring._deferredActions.Add(action);
            else
                action();
        }

        private void SetMaxSideLength(double width)
        {
            MaxSideLength = width <= 20 ? 20 : width; } private void SetEllipseDiameter(double width) { EllipseDiameter = width / 8; } private void SetEllipseOffset(double width) { EllipseOffset = new Thickness(0, width / 2, 0, 0); } private static void IsLargeChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { var ring = dependencyObject as ProgressRing; if (ring == null) return; ring.UpdateLargeState(); } private void UpdateLargeState() { Action action; if (IsLarge) action = () => VisualStateManager.GoToState(this, "Large", true);
            else
                action = () => VisualStateManager.GoToState(this, "Small", true);

            if (_deferredActions != null)
                _deferredActions.Add(action);

            else
                action();
        }

        private void OnSizeChanged(object sender, SizeChangedEventArgs sizeChangedEventArgs)
        {
            BindableWidth = ActualWidth;
        }

        private static void IsActiveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            var ring = dependencyObject as ProgressRing;
            if (ring == null)
                return;

            ring.UpdateActiveState();
        }

        private void UpdateActiveState()
        {
            Action action;

            if (IsActive)
                action = () => VisualStateManager.GoToState(this, "Active", true);
            else
                action = () => VisualStateManager.GoToState(this, "Inactive", true);

            if (_deferredActions != null)
                _deferredActions.Add(action);

            else
                action();
        }

        public override void OnApplyTemplate()
        {
            //make sure the states get updated
            UpdateLargeState();
            UpdateActiveState();
            base.OnApplyTemplate();
            if (_deferredActions != null)
                foreach (var action in _deferredActions)
                    action();
            _deferredActions = null;
        }
    }

    internal class WidthToMaxSideLengthConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is double)
            {
                var width = (double)value;
                return width <= 20 ? 20 : width;
            }

            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

ProgressRing.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:ProgressRingTest.Controls">

<Style TargetType="controls:ProgressRing">
        <Setter Property="Foreground" Value="{DynamicResource AccentColorBrush}" />
        <Setter Property="IsHitTestVisible" Value="False" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="MinHeight" Value="20" />
        <Setter Property="MinWidth" Value="20" />
        <Setter Property="Height" Value="60" />
        <Setter Property="Width" Value="60" />
        <Setter Property="IsTabStop" Value="False" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="controls:ProgressRing">
                    <Border Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
                        <Border.Resources>

<Style x:Key="ProgressRingEllipseStyle" TargetType="Ellipse">
                                <Setter Property="Opacity" Value="0" />
                                <Setter Property="HorizontalAlignment" Value="Left" />
                                <Setter Property="VerticalAlignment" Value="Top" />
                            </Style>


                        </Border.Resources>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="SizeStates">
                                <VisualState x:Name="Large">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="SixthCircle" Storyboard.TargetProperty="Visibility">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Small" />
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="ActiveStates">
                                <VisualState x:Name="Inactive" />
                                <VisualState x:Name="Active">
                                    <Storyboard RepeatBehavior="Forever">
                                        <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="Ring" Storyboard.TargetProperty="Visibility">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E1" Storyboard.TargetProperty="Opacity" BeginTime="0">
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E2" Storyboard.TargetProperty="Opacity" BeginTime="00:00:00.167">
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E3" Storyboard.TargetProperty="Opacity" BeginTime="00:00:00.334">
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E4" Storyboard.TargetProperty="Opacity" BeginTime="00:00:00.501">
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E5" Storyboard.TargetProperty="Opacity" BeginTime="00:00:00.668">
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E6" Storyboard.TargetProperty="Opacity" BeginTime="00:00:00.835">
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                            <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E1R" BeginTime="0" Storyboard.TargetProperty="Angle">
                                            <SplineDoubleKeyFrame KeyTime="0" Value="-110" KeySpline="0.13,0.21,0.1,0.7" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="10" KeySpline="0.02,0.33,0.38,0.77" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="93" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="205" KeySpline="0.57,0.17,0.95,0.75" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="357" KeySpline="0,0.19,0.07,0.72" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="439" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="585" KeySpline="0,0,0.95,0.37" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E2R" BeginTime="00:00:00.167" Storyboard.TargetProperty="Angle">
                                            <SplineDoubleKeyFrame KeyTime="0" Value="-116" KeySpline="0.13,0.21,0.1,0.7" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="4" KeySpline="0.02,0.33,0.38,0.77" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="87" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="199" KeySpline="0.57,0.17,0.95,0.75" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="351" KeySpline="0,0.19,0.07,0.72" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="433" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="579" KeySpline="0,0,0.95,0.37" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E3R" BeginTime="00:00:00.334" Storyboard.TargetProperty="Angle">
                                            <SplineDoubleKeyFrame KeyTime="0" Value="-122" KeySpline="0.13,0.21,0.1,0.7" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-2" KeySpline="0.02,0.33,0.38,0.77" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="81" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="193" KeySpline="0.57,0.17,0.95,0.75" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="345" KeySpline="0,0.19,0.07,0.72" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="427" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="573" KeySpline="0,0,0.95,0.37" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E4R" BeginTime="00:00:00.501" Storyboard.TargetProperty="Angle">
                                            <SplineDoubleKeyFrame KeyTime="0" Value="-128" KeySpline="0.13,0.21,0.1,0.7" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-8" KeySpline="0.02,0.33,0.38,0.77" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="75" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="187" KeySpline="0.57,0.17,0.95,0.75" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="339" KeySpline="0,0.19,0.07,0.72" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="421" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="567" KeySpline="0,0,0.95,0.37" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E5R" BeginTime="00:00:00.668" Storyboard.TargetProperty="Angle">
                                            <SplineDoubleKeyFrame KeyTime="0" Value="-134" KeySpline="0.13,0.21,0.1,0.7" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-14" KeySpline="0.02,0.33,0.38,0.77" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="69" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="181" KeySpline="0.57,0.17,0.95,0.75" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="331" KeySpline="0,0.19,0.07,0.72" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="415" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="561" KeySpline="0,0,0.95,0.37" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="E6R" BeginTime="00:00:00.835" Storyboard.TargetProperty="Angle">
                                            <SplineDoubleKeyFrame KeyTime="0" Value="-140" KeySpline="0.13,0.21,0.1,0.7" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-20" KeySpline="0.02,0.33,0.38,0.77" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="63" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="175" KeySpline="0.57,0.17,0.95,0.75" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="325" KeySpline="0,0.19,0.07,0.72" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="409" />
                                            <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="555" KeySpline="0,0,0.95,0.37" />
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Grid x:Name="Ring" Margin="{TemplateBinding Padding}" MaxWidth="{Binding MaxSideLength, RelativeSource={RelativeSource Mode=TemplatedParent}}" MaxHeight="{Binding MaxSideLength, RelativeSource={RelativeSource Mode=TemplatedParent}}" Visibility="Collapsed" RenderTransformOrigin=".5,.5" FlowDirection="LeftToRight">

                            <Canvas RenderTransformOrigin=".5,.5">
                                <Canvas.RenderTransform>
                                    <RotateTransform x:Name="E1R" />
                                </Canvas.RenderTransform>
                                <Ellipse x:Name="E1" Style="{StaticResource ProgressRingEllipseStyle}" Width="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Height="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Margin="{Binding EllipseOffset, RelativeSource={RelativeSource Mode=TemplatedParent}}" Fill="{TemplateBinding Foreground}" />
                            </Canvas>
                            <Canvas RenderTransformOrigin=".5,.5">
                                <Canvas.RenderTransform>
                                    <RotateTransform x:Name="E2R" />
                                </Canvas.RenderTransform>
                                <Ellipse x:Name="E2" Style="{StaticResource ProgressRingEllipseStyle}" Width="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Height="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Margin="{Binding EllipseOffset, RelativeSource={RelativeSource Mode=TemplatedParent}}" Fill="{TemplateBinding Foreground}" />
                            </Canvas>
                            <Canvas RenderTransformOrigin=".5,.5">
                                <Canvas.RenderTransform>
                                    <RotateTransform x:Name="E3R" />
                                </Canvas.RenderTransform>
                                <Ellipse x:Name="E3" Style="{StaticResource ProgressRingEllipseStyle}" Width="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Height="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Margin="{Binding EllipseOffset, RelativeSource={RelativeSource Mode=TemplatedParent}}" Fill="{TemplateBinding Foreground}" />
                            </Canvas>
                            <Canvas RenderTransformOrigin=".5,.5">
                                <Canvas.RenderTransform>
                                    <RotateTransform x:Name="E4R" />
                                </Canvas.RenderTransform>
                                <Ellipse x:Name="E4" Style="{StaticResource ProgressRingEllipseStyle}" Width="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Height="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Margin="{Binding EllipseOffset, RelativeSource={RelativeSource Mode=TemplatedParent}}" Fill="{TemplateBinding Foreground}" />
                            </Canvas>
                            <Canvas RenderTransformOrigin=".5,.5">
                                <Canvas.RenderTransform>
                                    <RotateTransform x:Name="E5R" />
                                </Canvas.RenderTransform>
                                <Ellipse x:Name="E5" Style="{StaticResource ProgressRingEllipseStyle}" Width="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Height="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Margin="{Binding EllipseOffset, RelativeSource={RelativeSource Mode=TemplatedParent}}" Fill="{TemplateBinding Foreground}" />
                            </Canvas>
                            <Canvas RenderTransformOrigin=".5,.5" Visibility="Collapsed" x:Name="SixthCircle">
                                <Canvas.RenderTransform>
                                    <RotateTransform x:Name="E6R" />
                                </Canvas.RenderTransform>
                                <Ellipse x:Name="E6" Style="{StaticResource ProgressRingEllipseStyle}" Width="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Height="{Binding EllipseDiameter, RelativeSource={RelativeSource Mode=TemplatedParent}}" Margin="{Binding EllipseOffset, RelativeSource={RelativeSource Mode=TemplatedParent}}" Fill="{TemplateBinding Foreground}" />
                            </Canvas>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>