Local image caching with custom control in WPF

Recently I needed my WPF application to cache images locally, when there’s connectivity issues or data is not updated. I searched web if there’s a standard way to do this, but it seems like you can do whatever you like and then I found a post on stackoverflow, where the same topic was discussed. Based on this I made custom control which:

  • can download images asynchronously and get them from cache if image was downloaded
  • is thread safe
  • has dependency property to which you can bind to
  • update images, providing new names in initial feed (don’t forget to maintain cache clean operation, e.g. you can parse your feed and asynchronously delete images with no links in feed)

Class of custom control is below:

public class CachedImage : Image
{
    static CachedImage()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(CachedImage), new FrameworkPropertyMetadata(typeof(CachedImage)));
    }

    public readonly static DependencyProperty ImageUrlProperty = DependencyProperty.Register("ImageUrl", typeof(string), typeof(CachedImage), new PropertyMetadata("", ImageUrlPropertyChanged));

    public string ImageUrl
    {
        get
        {
            return (string)GetValue(ImageUrlProperty);
        }
        set
        {
            SetValue(ImageUrlProperty, value);
        }
    }

    private static readonly object SafeCopy = new object();

    private static void ImageUrlPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var url = (String)e.NewValue;
        if (String.IsNullOrEmpty(url))
            return;

        var uri = new Uri(url);
        var localFile = String.Format(Path.Combine(Globals.CacheFolder, uri.Segments[uri.Segments.Length - 1]));
        var tempFile = String.Format(Path.Combine(Globals.CacheFolder, Guid.NewGuid().ToString()));

        if (File.Exists(localFile))
        {
            SetSource((CachedImage)obj, localFile);
        }
        else
        {
            var webClient = new WebClient();
            webClient.DownloadFileCompleted += (sender, args) =>
                                                    {
                                                        if (args.Error != null)
                                                        {
                                                            File.Delete(tempFile);
                                                            return;
                                                        }
                                                        if (File.Exists(localFile))
                                                            return;
                                                        lock (SafeCopy)
                                                        {
                                                            File.Move(tempFile, localFile);
                                                        }
                                                        SetSource((CachedImage)obj, localFile);
                                                    };

            webClient.DownloadFileAsync(uri, tempFile);
        }
    }

    private static void SetSource(Image inst, String path)
    {
        inst.Source = new BitmapImage(new Uri(path));
    }
}

DownloadFileAsync creates local file even if request failed, so I introduced temporary file, which is deleted if error occurred.

Globals.CacheFolder is just a class that holds static properties with paths and creates directories if they don’t exist, so you should replace it with your existing cache folder path.

Now you can use this CachedImage control in XAML, getting benefits from binding:

<Cache:CachedImage ImageUrl="{Binding Icon}"/>

Start TeamCity build via web request with Powershell script

There are cases when build triggers functionality is not enough. If you don’t want lets say do redundant commits to trigger a build, you can launch a build by web request. Unfortunately I could not find how to do it in official documentation, but after some searching I found a way. You just need to make web request to the following URL: http://YOURSERVER/httpAuth/action.html?add2Queue=btId, where btId is build type Id – unique identifier for each build configuration. To get it, you can just look for it in browser address bar, when clicking on build configuration, or use TeamCity REST  API for details.

Below is Powershell script, which starts TeamCity build by build type Id:

function Execute-HTTPRequestCommand() {
    param(
        [string] $url,
        [string] $username,
        [string] $password
    )

    $authInfo = $username + ":" + $password
    $authInfo = [System.Convert]::ToBase64String([System.Text.Encoding]::Default.GetBytes($authInfo))

    $webRequest = [System.Net.WebRequest]::Create($url)
    $webRequest.Headers["Authorization"] = "Basic " + $authInfo
    $webRequest.PreAuthenticate = $true

    [System.Net.WebResponse] $resp = $webRequest.GetResponse();
    $rs = $resp.GetResponseStream();
    [System.IO.StreamReader] $sr = New-Object System.IO.StreamReader -argumentList $rs;
    [string] $results = $sr.ReadToEnd();

    return $results;
}

function Execute-TeamCityAdd2Queue() {
    param(
        [string] $server,
        [string] $btId
    )
	$TeamCityURL = [System.String]::Format("{0}/httpAuth/action.html?add2Queue={1}", $server, $btId);
	Execute-HTTPRequestCommand $TeamCityURL "USER" "PASSWORD"
}

$server = "http://YOUR_SERVER"
$btId = "bt5"

Execute-TeamCityAdd2Queue $server $btId

Automatic TeamCity backup with Powershell

Automatic TeamCity backup is not configurable in UI, but you can use provided REST API and schedule powershell script launch on TeamCity machine or remote machine, which has access to TeamCity server.

You should make POST request, providing parameters in request string not in post data.
Get request to the same URL will return current backup status. See documenatation:

Data Backup

Start backup: POST http://teamcity:8111/httpAuth/app/rest/server/backup?includeConfigs=true&includeDatabase=true&includeBuildLogs=true&fileName=<fileName&gt; where <fileName> is the prefix of the file to save backup to. The file will be created in the default backup directory (see more).
Get current backup status (idle/running): GET http://teamcity:8111/httpAuth/app/rest/server/backup

Here is my code for scheduled task:

function Execute-HTTPPostCommand() {
    param(
        [string] $url,
		[string] $username,
		[string] $password
    )

	$authInfo = $username + ":" + $password
	$authInfo = [System.Convert]::ToBase64String([System.Text.Encoding]::Default.GetBytes($authInfo))

    $webRequest = [System.Net.WebRequest]::Create($url)
    $webRequest.ContentType = "text/html"
    $PostStr = [System.Text.Encoding]::Default.GetBytes("")
    $webrequest.ContentLength = $PostStr.Length
	$webRequest.Headers["Authorization"] = "Basic " + $authInfo
    $webRequest.PreAuthenticate = $true
    $webRequest.Method = "POST"

    $requestStream = $webRequest.GetRequestStream()
    $requestStream.Write($PostStr, 0, $PostStr.length)
    $requestStream.Close()

    [System.Net.WebResponse] $resp = $webRequest.GetResponse();
    $rs = $resp.GetResponseStream();
    [System.IO.StreamReader] $sr = New-Object System.IO.StreamReader -argumentList $rs;
    [string] $results = $sr.ReadToEnd();

    return $results;
}

function Execute-TeamCityBackup() {
    param(
        [string] $server,
		[string] $addTimestamp,
		[string] $includeConfigs,
		[string] $includeDatabase,
		[string] $includeBuildLogs,
		[string] $includePersonalChanges,
		[string] $fileName
    )
	$TeamCityURL = [System.String]::Format("{0}/httpAuth/app/rest/server/backup?addTimestamp={1}&includeConfigs={2}&includeDatabase={3}&includeBuildLogs={4}&includePersonalChanges={5}&fileName={6}",
											$server,
											$addTimestamp,
											$includeConfigs,
											$includeDatabase,
											$includeBuildLogs,
											$includePersonalChanges,
											$fileName);

	Execute-HTTPPostCommand $TeamCityURL "USER" "PASSWORD"
}

$server = "http://YOUR_SERVER"
$addTimestamp = $true
$includeConfigs = $true
$includeDatabase = $true
$includeBuildLogs = $true
$includePersonalChanges = $true
$fileName = "TeamCity_Backup_"

Execute-TeamCityBackup $server $addTimestamp $includeConfigs $includeDatabase $includeBuildLogs $includePersonalChanges $fileName

I’m using TeamCity 7.1, but previous versions also provide REST API.

Why every developer should have a blog?

Well, as many people find writing a blog on professional topic or personal experience a natural thing, there are advantages, may not seem obvious, just to state some:

  • It shows that you love things you do. You’re not paid for it, but have professional interest.
  • It is the way to communicate to others, discuss things, share ideas, come up with better solutions.
  • When you write down/describe a problem or an idea it naturally leads to better understanding and improvements to existing solutions.
  • You just do not want to forget things you did and keep track of your experience. So you can easily find some code snippets, approaches or ideas.
  • You may increase your confidence on some topics.

So I’m starting to keep track of things I investigate, research, working on or find interesting.