Click here to Skip to main content
14,270,297 members

WPF Apps Screen

Rate this:
4.90 (46 votes)
Please Sign up or sign in to vote.
4.90 (46 votes)
7 Apr 2016CPOL
A WPF application for listing, searching for and launching installed apps

Image 1

Introduction

This article's project is a slight replica of the Windows 8.1 apps screen, displaying a list of applications accessible from the Start menu, allowing filtering of the list using a search text box, and enabling the launching of those apps.

Background

The project makes use of the MVVM pattern, Unity is used for dependency injection, and Rx is used to maintain a responsive user interface when generating the list of apps.

Models

The project contains a single class that is used as a model. That class is the AppFile class .

public class AppFile
{
    public string Name { get; set; }
    public Icon Icon { get; set; }
    public string Path { get; set; }
}
Public Class AppFile
    Property Name As String
    Property Icon As Icon
    Property Path As String
End Class

Every item displayed in the apps list is an AppFile object.

Services

The StartMenuService class contains a function that generates and returns a collection of AppFile objects. The class implements the IAppService interface.

public interface IAppsService
{
    IEnumerable<AppFile> GetApps();
    void LaunchApp(string path);
}
Public Interface IAppsService
    Function GetApps() As IEnumerable(Of AppFile)
    Sub LaunchApp(ByVal path As String)
End Interface
public class StartMenuService : IAppsService
{
    private string sharedDir =
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu), "Programs");
    private string userDir =
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs");

    public IEnumerable<AppFile> GetApps()
    {
        var sharedFiles = Directory.EnumerateFiles(sharedDir, "*", SearchOption.AllDirectories).
            Where((f) => Path.GetExtension(f) == ".lnk");
        var userFiles = Directory.EnumerateFiles(userDir, "*", SearchOption.AllDirectories).
            Where((f) => Path.GetExtension(f) == ".lnk");

        var files = userFiles.Concat(sharedFiles).Select((f) => new FileInfo(f)).Distinct(new FileInfoComparer());
        var apps = files.Select((f) => new AppFile
        {
            Name = Path.GetFileNameWithoutExtension(f.FullName),
            Path = GetTargetPath(f.FullName),
            Icon = GetTargetIcon(GetTargetPath(f.FullName))
        });

        apps = apps.Where((t) => t.Path != string.Empty && t.Icon != null && t.Name.Contains("install") != true &&
        Path.GetExtension(t.Path).Contains(".exe", StringComparison.CurrentCultureIgnoreCase));

        return apps;
    }

    public void LaunchApp(string path)
    {
        if (!String.IsNullOrEmpty(path)) { Process.Start(path); }
    }

    private string GetTargetPath(string lnk)
    {
        WshShell ws = new WshShell();
        IWshShortcut shortcut;
        string target;

        try
        {
            shortcut = (IWshShortcut)ws.CreateShortcut(lnk);
            target = shortcut.TargetPath;
        }
        catch (Exception) { target = null; }
        return target;
    }

    private Icon GetTargetIcon(string target)
    {
        Icon ico;

        if (target != null)
        {
            try { ico = Icon.ExtractAssociatedIcon(target); }
            catch (Exception) { ico = null; }
        }
        else { ico = null; }
        return ico;
    }
}
Public Class StartMenuService
    Implements IAppsService

    Private sharedDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu), "Programs")
    Private userDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs")

    Public Function GetApps() As IEnumerable(Of AppFile) Implements IAppsService.GetApps
        Dim sharedFiles = Directory.EnumerateFiles(sharedDir, "*", SearchOption.AllDirectories).
            Where(Function(f) Path.GetExtension(f) = ".lnk")
        Dim userFiles = Directory.EnumerateFiles(userDir, "*", SearchOption.AllDirectories).
            Where(Function(f) Path.GetExtension(f) = ".lnk")

        Dim files = userFiles.Concat(sharedFiles).Select(Function(f) New FileInfo(f)).Distinct(New FileInfoComparer)
        Dim apps = files.Select(Function(f) New AppFile With {
                                    .Name = Path.GetFileNameWithoutExtension(f.FullName),
                                    .Path = GetTargetPath(f.FullName),
                                    .Icon = GetTargetIcon(GetTargetPath(f.FullName))})

        apps = apps.Where(Function(t) t.Path <> String.Empty AndAlso t.Icon IsNot Nothing AndAlso
                              t.Name.Contains("install") <> True AndAlso
                              Path.GetExtension(t.Path).Contains(".exe", StringComparison.CurrentCultureIgnoreCase))
        Return apps
    End Function

    Public Sub LaunchApp(path As String) Implements IAppsService.LaunchApp
        If Not String.IsNullOrEmpty(path) Then
            Process.Start(path)
        End If
    End Sub

    Private Function GetTargetPath(ByVal lnk As String) As String
        Dim ws As New WshShell
        Dim shortcut As IWshShortcut
        Dim target As String

        Try
            shortcut = CType(ws.CreateShortcut(lnk), IWshShortcut)
            target = shortcut.TargetPath
        Catch ex As Exception
            target = Nothing
        End Try
        Return target
    End Function

    Private Function GetTargetIcon(ByVal target As String) As Icon
        Dim ico As Icon

        If target IsNot Nothing Then
            Try
                ico = Icon.ExtractAssociatedIcon(target)
            Catch ex As Exception
                ico = Nothing
            End Try
        Else
            ico = Nothing
        End If
        Return ico
    End Function
End Class

The GetApps() method returns an IEnumerable<AppFile> after enumerating the files in the shared and user's Start menu folders. The GetTargetPath() function gets the path of an executable associated with a .lnk file while the GetTargetIcon()  function is used to extract the icon of an executable.

ViewModel

There's only one View Model in the project, AppsViewModel, and it has a single dependency that will be injected through constuctor injection. It is in one of the methods in this class that Rx is used to iterate through a collection of AppFile objects and return results that are added to an ObservableCollection<AppFile>.

public class AppsViewModel : ViewModelBase
{
    private ObservableCollection<AppFile> _apps;
    public ObservableCollection<AppFile> Apps
    {
        get { return _apps; }
    }

    private ICommand _startAppCommand;
    public ICommand StartAppCommand
    {
        get
        {
            if (_startAppCommand == null) { _startAppCommand = new RelayCommand(LaunchApp); }
            return _startAppCommand;
        }
    }

    private ICommand _getAppsCommand;
    public ICommand GetAppsCommand
    {
        get
        {
            if (_getAppsCommand == null) { _getAppsCommand = new RelayCommand(GetApps); }
            return _getAppsCommand;
        }
    }

    private string _filter = string.Empty;
    public string Filter
    {
        set
        {
            _filter = value;
            if (appsView != null) { appsView.Refresh(); }
        }
    }

    private IAppsService appsService;
    private ICollectionView appsView;

    public AppsViewModel(IAppsService service)
    {
        appsService = service;
        _apps = new ObservableCollection<AppFile>();
        appsView = CollectionViewSource.GetDefaultView(Apps);
        appsView.Filter = (f) => { return (f as AppFile).Name.Contains(_filter, StringComparison.CurrentCultureIgnoreCase); };
    }

    private void LaunchApp(object o)
    {
        appsService.LaunchApp((o as AppFile).Path);
    }

    private void GetApps(object o)
    {
        if (_apps.Count > 0) { _apps.Clear(); };

        var ob = appsService.GetApps().ToObservable().SubscribeOn(Scheduler.Default).ObserveOn(SynchronizationContext.Current);
        ob.Subscribe((f) => _apps.Add(f));
    }

}
Public Class AppsViewModel
    Inherits ViewModelBase

    Private _apps As ObservableCollection(Of AppFile)
    Public ReadOnly Property Apps As ObservableCollection(Of AppFile)
        Get
            Return _apps
        End Get
    End Property

    Private _startAppCommand As ICommand
    Public ReadOnly Property StartAppCommand As ICommand
        Get
            If _startAppCommand Is Nothing Then
                _startAppCommand = New RelayCommand(AddressOf StartApp)
            End If
            Return _startAppCommand
        End Get
    End Property

    Private _getAppsCommand As ICommand
    Public ReadOnly Property GetAppsCommand As ICommand
        Get
            If _getAppsCommand Is Nothing Then
                _getAppsCommand = New RelayCommand(AddressOf GetApps)
            End If
            Return _getAppsCommand
        End Get
    End Property

    Private _filter As String = String.Empty
    Public WriteOnly Property Filter As String
        Set(value As String)
            _filter = value
            If appsView IsNot Nothing Then
                appsView.Refresh()
            End If
        End Set
    End Property

    Private appsService As IAppsService
    Private appsView As ICollectionView

    Public Sub New(service As IAppsService)
        appsService = service
        _apps = New ObservableCollection(Of AppFile)
        appsView = CollectionViewSource.GetDefaultView(_apps)
        appsView.Filter = Function(f) CType(f, AppFile).Name.Contains(_filter, StringComparison.CurrentCultureIgnoreCase)
    End Sub

    Private Sub StartApp(ByVal o As Object)
        Dim path = CType(o, AppFile).Path
        appsService.LaunchApp(path)
    End Sub

    Private Sub GetApps(ByVal o As Object)
        If _apps.Count > 0 Then
            _apps.Clear()
        End If

        Dim ob = appsService.GetApps().ToObservable().SubscribeOn(Scheduler.Default).ObserveOn(SynchronizationContext.Current)
        ob.Subscribe(Sub(f) _apps.Add(f))
    End Sub
End Class

Notice the use of Rx extension methods in the GetApps() method. The IEnumerable<AppFile> returned by the GetApps() method of the service is converted to an IObservable<AppFile> using Reactive Extension's ToObservable() method. The iteration of the collection is set to be done on a background thread, by passing Scheduler.Default to the SubscribeOn() extension method and the results of the iteration are set to be received on the Dispatcher by passing the current synchronization context to the ObserveOn() extension method. This will ensure that an item is received immeadiately it is available while maintaining a responsive UI. Finally the Subcribe() method is called and passed a delegate where results are added to an ObservableCollection<AppFile>.

View Model Locator

In the ViewModelLocator class Unity is used to register dependencies and resolve instances.

class ViewModelLocator
{
    private UnityContainer container;

    public ViewModelLocator()
    {
        container = new UnityContainer();
        container.RegisterType<IAppsService, StartMenuService>();
    }

    public AppsViewModel AppsVM
    {
        get { return container.Resolve<AppsViewModel>(); }
    }
}
Public Class ViewModelLocator
    Private container As UnityContainer

    Public Sub New()
        container = New UnityContainer
        container.RegisterType(Of IAppsService, StartMenuService)()
    End Sub

    Public ReadOnly Property AppsVM As AppsViewModel
        Get
            Return container.Resolve(Of AppsViewModel)()
        End Get
    End Property
End Class

The view-model locator is declared as an application level resource.

<utils:ViewModelLocator x:Key="VMLocator"/>

View

I've made use of MahApps.Metro to give the application's window a Metro/Modern feel. The list of apps is displayed in an ItemsControl and a TextBox, with a custom style, is used for specifying the filter criteria.

<Controls:MetroWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

                      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

                      xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"

                      xmlns:utils="clr-namespace:WPF_Apps_Screen_CS.Utils"                      

                      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

                      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

                      xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

                      mc:Ignorable="d" x:Class="WPF_Apps_Screen_CS.MainWindow"

                      Title="MainWindow" Height="603" Width="987" ShowTitleBar="False"

                      WindowStartupLocation="CenterScreen" EnableDWMDropShadow="True"

                      PreviewKeyDown="MainWindow_PreviewKeyDown">   

    <Controls:MetroWindow.Background>
        <ImageBrush ImageSource="Images/DarkWood.jpg"/>
    </Controls:MetroWindow.Background>

    <Controls:MetroWindow.DataContext>
        <Binding Source="{StaticResource VMLocator}" Path="AppsVM"/>
    </Controls:MetroWindow.DataContext>

    <i:Interaction.Triggers>
        <i:EventTrigger>
            <i:InvokeCommandAction Command="{Binding GetAppsCommand, Mode=OneWay}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="90"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock HorizontalAlignment="Left" Margin="50,0,0,0" TextWrapping="Wrap" Text="Apps" 

                   VerticalAlignment="Bottom" Height="70" Foreground="White" FontSize="48"

                   FontFamily="Segoe UI Light"/>
        
        <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
            <i:Interaction.Behaviors>
                <utils:MouseWheelScrollBehavior/>
            </i:Interaction.Behaviors>
            <ItemsControl ItemsPanel="{StaticResource TilesPanel}" ItemsSource="{Binding Apps}"

                          ItemTemplate="{StaticResource TileTemplate}" Margin="48,15,0,15"/>
        </ScrollViewer>

        <TextBox x:Name="FilterTextBox" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,20,18" 

                 TextWrapping="Wrap" Width="178" FontSize="14" FontFamily="Segoe UI" Height="26"

                 Style="{DynamicResource FilterTextBoxStyle}"

                 Text="{Binding Filter, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</Controls:MetroWindow>

The DataTemplate that is used to set the ItemTemplate property of the ItemsControl is defined in the App.xaml / Application.xaml file.

<DataTemplate x:Key="TilesDataTemplate">
    <DataTemplate.Resources>
        <Storyboard x:Key="ShowTileBackground">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)"

                   Storyboard.TargetName="TileBackroundRct">
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Key="HideTileBackground">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)"

                   Storyboard.TargetName="TileBackroundRct">
                <SplineDoubleKeyFrame KeyTime="0" Value="1"/>
                <SplineDoubleKeyFrame KeyTime="0:0:0.2" Value="0"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Key="TileBounce">
            <DoubleAnimationUsingKeyFrames

                   Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"

                   Storyboard.TargetName="TileGrid">
                <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="0.867"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames

                   Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"

                   Storyboard.TargetName="TileGrid">
                <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="0.867"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
    </DataTemplate.Resources>
    <Grid x:Name="TileGrid" Width="210" Height="50" Margin="0,0,15,10" RenderTransformOrigin="0.5,0.5">
        <Grid.RenderTransform>
            <TransformGroup>
                <ScaleTransform/>
                <SkewTransform/>
                <RotateTransform/>
                <TranslateTransform/>
            </TransformGroup>
        </Grid.RenderTransform>
        <Rectangle x:Name="TileBackroundRct" Fill="#FF1F85BF" Height="Auto" Stroke="#FF2BA5EC" Width="Auto" Opacity="0"/>
        <StackPanel Margin="5" Orientation="Horizontal">
        	<Border BorderBrush="#FF454F57" BorderThickness="1" HorizontalAlignment="Left"

                   Height="40" Width="40" Background="#FF2E3942">
        		<Image Margin="5" Source="{Binding Icon, Converter={StaticResource IconBitmapSourceConverter}}"/>
        	</Border>
        	<TextBlock Text="{Binding Name}" HorizontalAlignment="Left" TextWrapping="Wrap" Width="145"

                   TextTrimming="CharacterEllipsis" Foreground="#FFE4E4E4" FontSize="14" Margin="10,0,5,0"/>
        </StackPanel>
        <Button x:Name="TileButton" Command="{Binding DataContext.StartAppCommand,
               RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"

               CommandParameter="{Binding}" Opacity="0"/>
    </Grid>
    <DataTemplate.Triggers>
        <EventTrigger RoutedEvent="ButtonBase.Click" SourceName="TileButton">
        	<BeginStoryboard x:Name="TileBounce_BeginStoryboard" Storyboard="{StaticResource TileBounce}"/>
        </EventTrigger>
        <EventTrigger RoutedEvent="Mouse.MouseEnter">
        	<BeginStoryboard Storyboard="{StaticResource ShowTileBackground}"/>
        </EventTrigger>
        <EventTrigger RoutedEvent="Mouse.MouseLeave">
        	<BeginStoryboard x:Name="HideTileBackground_BeginStoryboard" Storyboard="{StaticResource HideTileBackground}"/>
        </EventTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

The Storyboards were created in Expression Blend and are used to animate some template elements in response to mouse events.

The binding for the Source property of the Image control uses a converter to convert an Icon to a BitmapSource. To do this the converter uses a custom extension method.

public static class Extensions
{        
    public static BitmapSource ToBitmapSource(this Icon ico)
    {
        IntPtr hIcon = ico.Handle;
        BitmapSource bmpSrc = null;

        try
        {
            bmpSrc = Imaging.CreateBitmapSourceFromHIcon(hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
        }
        catch (Exception)
        {
            bmpSrc = null;
        }

        return bmpSrc;
    }                
    ...
}
Module Extensions
    <Extension()>
    Function ToBitmapSource(ByVal ico As Icon) As BitmapSource
        Dim hIcon As IntPtr = ico.Handle
        Dim bmpSrc As BitmapSource

        Try
            bmpSrc = Imaging.CreateBitmapSourceFromHIcon(hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions())
        Catch ex As Exception
            bmpSrc = Nothing
        End Try

        Return bmpSrc
    End Function
    ...
End Module

Horizontal Mouse Wheel Scrolling

The ScrollViewer by default doesn't support horizontal mouse wheel scrolling but this is enabled using a custom behavior.

public class MouseWheelScrollBehavior: Behavior<ScrollViewer>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
    }

    protected void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        if (e.Delta > 0)
        {
            AssociatedObject.LineLeft();
            e.Handled = true;
        }
        else
        {
            AssociatedObject.LineRight();
            e.Handled = true;
        }
    }
}
Public Class MouseWheelScrollBehavior
    Inherits Behavior(Of ScrollViewer)

    Protected Overrides Sub OnAttached()
        MyBase.OnAttached()
        AddHandler AssociatedObject.PreviewMouseWheel, AddressOf AssociatedObject_PreviewMouseWheel
    End Sub

    Protected Overrides Sub OnDetaching()
        MyBase.OnDetaching()
        AddHandler AssociatedObject.PreviewMouseWheel, AddressOf AssociatedObject_PreviewMouseWheel
    End Sub

    Private Sub AssociatedObject_PreviewMouseWheel(sender As Object, e As MouseWheelEventArgs)
        If e.Delta > 0 Then
            AssociatedObject.LineLeft()
            e.Handled = True
        Else
            AssociatedObject.LineRight()
            e.Handled = True
        End If
    End Sub
End Class

The behavior handles the ScrollViewer's PreviewMouseWheel event calling either its LineLeft() or LineRight() method depending on the mouse wheel's delta value.

Directing all Keystrokes to the TextBox

To ensure key presses are directed to the textbox, keyboard focus is set on the textbox when a key is pressed.

public partial class MainWindow : MetroWindow
{
    ...
    private void MainWindow_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (FilterTextBox.IsKeyboardFocused != true)
        {
            Keyboard.Focus(FilterTextBox);
        }
    }
}
Class MainWindow
    Private Sub MainWindow_PreviewKeyDown(sender As Object, e As KeyEventArgs) Handles Me.PreviewKeyDown
        If FilterTextBox.IsKeyboardFocused <> True Then
            Keyboard.Focus(FilterTextBox)
        End If
    End Sub
End Class

Conclusion

That's it. I hope you have learnt something useful from this article. If you want to know more about Rx I recommend you check out Lee Campell's free online book, Introduction to Rx.

History

  • 14th Jan 2015: Initial post,
  • 8th April 2016: Update

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Meshack Musundi
Software Developer
Kenya Kenya
Meshack is a software developer with a passion for WPF.

Awards,

  • CodeProject MVP 2013
  • CodeProject MVP 2012

Comments and Discussions

 
PraiseThx Pin
Burak Tunçbilek20-Jan-17 3:41
memberBurak Tunçbilek20-Jan-17 3:41 
QuestionVery Nice 5/5 Pin
dvbr20-Apr-16 21:11
memberdvbr20-Apr-16 21:11 
AnswerRe: Very Nice 5/5 Pin
Meshack Musundi20-Apr-16 21:30
professionalMeshack Musundi20-Apr-16 21:30 
GeneralMy vote of 5 Pin
D V L11-Feb-15 21:45
professionalD V L11-Feb-15 21:45 
GeneralRe: My vote of 5 Pin
Meshack Musundi12-Feb-15 8:33
professionalMeshack Musundi12-Feb-15 8:33 
GeneralMy vote of 5 Pin
Pooja Baraskar9-Feb-15 3:32
professionalPooja Baraskar9-Feb-15 3:32 
GeneralRe: My vote of 5 Pin
Meshack Musundi9-Feb-15 7:51
professionalMeshack Musundi9-Feb-15 7:51 
QuestionMy vote of 5 Pin
Kenneth Haugland6-Feb-15 23:27
professionalKenneth Haugland6-Feb-15 23:27 
GeneralRe: My vote of 5 Pin
Meshack Musundi7-Feb-15 2:27
professionalMeshack Musundi7-Feb-15 2:27 
GeneralMy vote of 5 Pin
jaytmunn16-Jan-15 5:54
memberjaytmunn16-Jan-15 5:54 
GeneralRe: My vote of 5 Pin
Meshack Musundi16-Jan-15 8:49
professionalMeshack Musundi16-Jan-15 8:49 
QuestionVery Nice article Pin
Sumit Jawale15-Jan-15 1:05
memberSumit Jawale15-Jan-15 1:05 
GeneralRe: Very Nice article Pin
Meshack Musundi15-Jan-15 8:09
professionalMeshack Musundi15-Jan-15 8:09 
GeneralMy vote of 5 Pin
Oleg A.Lukin14-Jan-15 19:59
memberOleg A.Lukin14-Jan-15 19:59 
GeneralRe: My vote of 5 Pin
Meshack Musundi15-Jan-15 8:08
professionalMeshack Musundi15-Jan-15 8:08 
GeneralMy vote of 5 Pin
Afzaal Ahmad Zeeshan14-Jan-15 8:55
mveAfzaal Ahmad Zeeshan14-Jan-15 8:55 
GeneralRe: My vote of 5 Pin
Meshack Musundi15-Jan-15 8:07
professionalMeshack Musundi15-Jan-15 8:07 
QuestionConstructive comment (I hope) Pin
Sacha Barber14-Jan-15 2:15
mvaSacha Barber14-Jan-15 2:15 
GeneralRe: Constructive comment (I hope) Pin
Meshack Musundi14-Jan-15 6:23
professionalMeshack Musundi14-Jan-15 6:23 
GeneralRe: Constructive comment (I hope) Pin
Meshack Musundi15-Jan-15 8:12
professionalMeshack Musundi15-Jan-15 8:12 
GeneralRe: Constructive comment (I hope) Pin
Sacha Barber15-Jan-15 19:54
mvaSacha Barber15-Jan-15 19:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 13 Jan 2015

Stats

40.8K views
2.8K downloads
66 bookmarked