Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Apps Screen

0.00/5 (No votes)
7 Apr 2016 1  
A WPF application for listing, searching for and launching installed apps

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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here