Search code examples
c#wpfmvvm-lightroutedcommand

WPF - Coupling a GlobalResource to a ViewModel


I have a WPF application with a NotifyIcon defined in a ResourceDictionary that is added to Application.Current.Resources on App start.

I am using the MVVM-Light framework and I want to bind the Command properties of a ContextMenu.MenuItems on the NotifyIcon to a public RelayCommand defined in a ViewModel.

I am comfortable with coupling a View to a ViewModel but how to I couple a Global Resource to a ViewModel?

Here is my attempt at getting this to work, just not sure if I'm on the correct lines or not...

When I run this code I get an error stating "Cannot find resource named 'Locator'. Resource names are case sensitive." This originates from the DataContext binding on the TaskBarIcon tag in NotificationIconResources.xaml

SingleInstanceManager ensures just one instance can be created

    public sealed class SingleInstanceManager : WindowsFormsApplicationBase
{
    [STAThread]
    public static void Main(string[] args)
    {
        (new SingleInstanceManager()).Run(args);
    }

    public SingleInstanceManager()
    {
        IsSingleInstance = true;
    }

    public ControllerApp App { get; private set; }

    protected override bool OnStartup(StartupEventArgs e)
    {
        App = new ControllerApp();
        App.Run();
        return false;
    }

    protected override void OnStartupNextInstance(StartupNextInstanceEventArgs eventArgs)
    {
        base.OnStartupNextInstance(eventArgs);
        App.MainWindow.Activate();
        App.ProcessArgs(eventArgs.CommandLine.ToArray(), false);
    }
}

ControllerApp replaces App.xaml and App.xaml.cs

public class ControllerApp : Application
{
    public MainWindow window { get; private set; }
    bool startMinimized = false;
    private TaskbarIcon tb;

    public ControllerApp()
        : base()
    { }

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        DispatcherHelper.Initialize();

        ResourceDictionary dict = new ResourceDictionary();
        dict.Source = new Uri("NotificationIconResources.xaml", UriKind.Relative);
        Application.Current.Resources.MergedDictionaries.Add(dict);

        ViewModel.ViewModelLocator vmLocator = new ViewModel.ViewModelLocator();
        Application.Current.Resources.Add("Locator", vmLocator);

        window = new MainWindow();
        ProcessArgs(e.Args, true);

        //initialize NotifyIcon
        tb = (TaskbarIcon)FindResource("ItemNotifyIcon");

        if (startMinimized)
        {
            window.WindowState = WindowState.Minimized;
        }

        window.Show();
    }

    protected override void OnExit(ExitEventArgs e)
    {
        base.OnExit(e);

        tb.Dispose();
    }

    public void ProcessArgs(string[] args, bool firstInstance)
    {

    }
}

NotificationIconResources.xaml is the resource dictionary defining the NotifyIcon

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:tb="http://www.hardcodet.net/taskbar">

    <tb:TaskbarIcon x:Key="ItemNotifyIcon"
                    IconSource="/Controller;component/Images/ItemRunning.ico"
                    IsNoWaitForDoubleClick="True"
                    ToolTipText="Item is running" 
                    DataContext="{Binding NotifyIcon, Source={StaticResource Locator}}">

        <tb:TaskbarIcon.ContextMenu>
            <ContextMenu>
                <MenuItem Header="Open Control Panel" />
                <Separator />
                <MenuItem Header="Start Item" Command="{Binding Path=StartServiceCommand}" />
                <MenuItem Header="Pause Item" />
                <MenuItem Header="Stop Item" Command="{Binding Path=StopServiceCommand}" />
                <Separator />
                <MenuItem Header="Close" />
            </ContextMenu>
        </tb:TaskbarIcon.ContextMenu>

    </tb:TaskbarIcon>
</ResourceDictionary>

NotifyIconViewModel contains the RelayCommands I want to bind to

    /// <summary>
/// This class contains properties that the NotifyIcon View can data bind to.
/// <para>
/// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel.
/// </para>
/// <para>
/// You can also use Blend to data bind with the tool's support.
/// </para>
/// <para>
/// See http://www.galasoft.ch/mvvm/getstarted
/// </para>
/// </summary>
public class NotifyIconViewModel : ViewModelBase
{
    private ServiceController sc;

    public string Welcome
    {
        get
        {
            return "Welcome to MVVM Light";
        }
    }

    /// <summary>
    /// Initializes a new instance of the NotifyIconViewModel class.
    /// </summary>
    public NotifyIconViewModel()
    {
        if (IsInDesignMode)
        {
            // Code runs in Blend --> create design time data.
        }
        else
        {
            sc = new ServiceController("Item");
        }
    }

    #region Public Commands

    private RelayCommand _startServiceCommand = null;

    public RelayCommand StartServiceCommand
    {
        get
        {
            if (_startServiceCommand == null)
            {
                _startServiceCommand = new RelayCommand(
                    () => this.OnStartServiceCommand(),
                    () => (sc.Status == ServiceControllerStatus.Stopped));
            }
            return _stopServiceCommand;
        }
    }

    private void OnStartServiceCommand()
    {
        try
        {
            sc.Start();
        }
        catch (Exception ex)
        {
            // notify user if there is any error
            AppMessages.RaiseErrorMessage.Send(ex);
        }
    }

    private RelayCommand _stopServiceCommand = null;

    public RelayCommand StopServiceCommand
    {
        get
        {
            if (_stopServiceCommand == null)
            {
                _stopServiceCommand = new RelayCommand(
                    () => this.OnStopServiceCommand(),
                    () => (sc.CanStop && sc.Status == ServiceControllerStatus.Running));
            }
            return _stopServiceCommand;
        }
    }

    private void OnStopServiceCommand()
    {
        try
        {
            sc.Stop();
        }
        catch (Exception ex)
        {
            // notify user if there is any error
            AppMessages.RaiseErrorMessage.Send(ex);
        }
    }

    #endregion

    ////public override void Cleanup()
    ////{
    ////    // Clean up if needed

    ////    base.Cleanup();
    ////}
}

Solution

  • Given that you've declared the NotifyIcon at the Application level, you're not going to be able to have it inherit another view's ViewModel since it's not in the visual tree of any views. Your best bet is probably to give the NotifyIcon its own ViewModel with commands defined on it and then handle the communication between the ViewModels rather than across the UI.

    If you need it to bind to a specific view's ViewModel you could also consider declaring it in that view instead of globally, in which case it can automatically inherit the DataContext you're trying to use (but it will be opened and closed with that view).