4

I was wondering how to integrate NotifyIcon with Caliburn.Micro.

I'm trying to integrate with Caliburn using low level Caliburn APIs. Here are the classes:

ITrayIconManager

public interface ITrayIconManager
{
    ITrayIcon GetOrCreateFor<T>();
}

ITrayIcon (wrapper around TaskbarIcon from WPF NotifyIcon)

 public interface ITrayIcon : IDisposable
{
    void ShowBalloonTip(string title, string message, BalloonIcon symbol);
    void Show();
    void Hide();
}

ISetTrayIconInstance

public interface ISetTrayIconInstance
{
    ITrayIcon Icon { set; }
}

TrayIconWrapper

public class TrayIconWrapper : ITrayIcon
{
    private readonly TaskbarIcon icon;

    public TrayIconWrapper(TaskbarIcon icon)
    {
        this.icon = icon;
    }

    public bool IsDisposed { get; private set; }

    public void Dispose()
    {
        icon.Dispose();
        IsDisposed = true;
    }

    public void Show()
    {
        icon.Visibility = Visibility.Visible;
    }

    public void Hide()
    {
        icon.Visibility = Visibility.Collapsed;
    }

    public void ShowBalloonTip(string title, string message, BalloonIcon symbol)
    {
        icon.ShowBalloonTip(title, message, symbol);
    }
}

TrayIconManager

public class TrayIconManager : ITrayIconManager
{
    private readonly IDictionary<WeakReference, WeakReference> icons;

    public TrayIconManager()
    {
        icons = new Dictionary<WeakReference, WeakReference>();
    }

    public ITrayIcon GetOrCreateFor<T>()
    {
        if (!icons.Any(i => i.Key.IsAlive && typeof(T).IsAssignableFrom(i.Key.Target.GetType())))
            return Create<T>();

        var reference = icons.First(i => i.Key.IsAlive && typeof(T).IsAssignableFrom(i.Key.Target.GetType())).Value;
        if (!reference.IsAlive)
            return Create<T>();

        var wrapper = (TrayIconWrapper)reference.Target;
        if (wrapper.IsDisposed)
            return Create<T>();

        return wrapper;
    }

    private ITrayIcon Create<T>()
    {
        var rootModel = IoC.Get<T>();
        var view = ViewLocator.LocateForModel(rootModel, null, null);
        var icon = view is TaskbarIcon ? (TaskbarIcon)view : new TaskbarIcon();
        var wrapper = new TrayIconWrapper(icon);

        ViewModelBinder.Bind(rootModel, view, null);
        SetIconInstance(rootModel, wrapper);
        icons.Add(new WeakReference(rootModel), new WeakReference(wrapper));

        return wrapper;
    }

    private void SetIconInstance(object rootModel, ITrayIcon icon)
    {
        var instance = rootModel as ISetTrayIconInstance;
        if (instance != null)
            instance.Icon = icon;
    }
}

This is the code, now how do I use it? This code relies on Caliburn View - ViewModel binding, that is, I need to create a ViewModel for TasbarkIcon and a View (which must be inherited from TaskbarIcon control):

TrayIconViewModel

public class TrayIconViewModel : IMainTrayIcon, ISetTrayIconInstance
{
    public TrayIconViewModel()
    {

    }

    public ITrayIcon Icon { get; set; }

    public void ShowWindow()
    {
        Icon.Hide();
        System.Windows.Application.Current.MainWindow.Show(); //very very bad :(
    }

ITrayIcon is the wrapper for TaskbarIcon control. Now I can call methods on it from my ViewModel, which is great.

TrayIconView (cal:Message:Attach doesn't work - the ShowWindow never gets hit)

<tb:TaskbarIcon x:Class="Communicator.Softphone.Views.TrayIconView"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
            xmlns:tb="http://www.hardcodet.net/taskbar"
            xmlns:cal="http://www.caliburnproject.org"
            mc:Ignorable="d" 
            d:DesignHeight="300" d:DesignWidth="300"
            IconSource="/Communicator.ControlLibrary;component/Assets/phone_icon.ico"
            ToolTipText="Secretária do Futuro - Comunicador"
            Visibility="Collapsed"
            cal:Message.Attach="[Event TrayLeftMouseDown] = [Action ShowWindow()]">

On my ShellViewModel (trayIcon is the wrapper around TaskbarIcon):

private ITrayIcon trayIcon;
protected override void OnActivate()
    {
        trayIcon = trayIconManager.GetOrCreateFor<IMainTrayIcon>();
        ActivateItem(containers.FirstOfType<IPhone>());
    }
public override void CanClose(Action<bool> callback)
    {
        trayIcon.Show();
        trayIcon.ShowBalloonTip("Comunicador", "Comunicador foi minimizado", BalloonIcon.Info);
        (GetView() as Window).Hide();
        callback(false);
    }

trayIcon.Show()is working, however trayIcon.ShowBallonTip(...) doesn't do anything, no errors, no nothing.

Issues summary:

  1. Binding Message.Attach is not working, although Caliburn output logging messages for this as it is working.
  2. Calling ShowBallonTip on the wrapper seems not to work, although it is calling the actual TaskbarIcon method. (it works without debugger attached)
JK.
  • 21,477
  • 35
  • 135
  • 214
JobaDiniz
  • 862
  • 1
  • 14
  • 32
  • 1
    I'm looking into this. Give me a minute – Frank Dec 21 '14 at 12:54
  • Sorry... I've edit it to make my case :) – JobaDiniz Dec 21 '14 at 12:55
  • Sorry but just asking can do you have reference to `System.Windows.Forms`, if so then you can initialize the NotifyIcon directly in the view model. Hope this might be a bit helpful. – D_Learning Feb 10 '15 at 15:28
  • I'm interested in using [wpf-notify-icon](http://www.hardcodet.net/wpf-notifyicon), which has a lot of features built-in for WPF; and not using NotifyIcon from `System.Windows.Forms` – JobaDiniz Feb 10 '15 at 18:43
  • Hello there, BrainCrumbz team here. I checked your reply on [CodeProject](http://www.codeproject.com/Articles/36468/WPF-NotifyIcon?msg=5016019#xx5016019xx). I'm not sure I really understood what you're up to, I got a little bit lost in all the wrappings etc. Did you have a look at our sample [Github project](https://github.com/BrainCrumbz/caliburn-micro-wpf-notifyicon-poc/), especially the 4th example? – superjos Mar 07 '15 at 16:37
  • I did. That won't help. What about methods ShowCustomBallon and ShowBallonTip? I need to somehow call those method from my view-model without actually having a reference to the TaskbarIcon. I manage to implement it.. only problem is binding Actions. Now I'm able to do this: `trayIcon.ShowBalloonTip(new AnyViewModel(), PopupAnimation.Slide, TimeSpan.FromSeconds(5));` It is really nice, because Caliburn will take care of instanciating the view of *AnyViewModel*. – JobaDiniz Mar 07 '15 at 18:43

2 Answers2

3

You could use the Event Aggregator to do what you want.

Documentation: http://caliburnmicro.com/documentation/event-aggregator

Add a field to your TaskbarViewModel for the Event Aggregator and add a constructor to accomodate the injection:

public class TaskbarViewModel : PropertyChangedBase, ITaskbar {
    private readonly IEventAggregator _eventAggregator;

    public TaskbarViewModel(IEventAggregator eventAggregator) {
        _eventAggregator = eventAggregator;
    }

    public void Show() {
        IsVisible = true;
        _eventAggregator.PublishOnUIThread("Your balloontip message");
    }

    /// Rest of the implementation
}

Implement the IHandle interface where you can access the TaskBarIcon and call the ShowBalloonTip method.

Roel van Westerop
  • 1,400
  • 8
  • 17
  • None of my viewmodel have access to the TaskBarIcon.. and should they? TaskBarIcon is a Control, it is view related... I could publish an event, but who would listen? Nobody... I guess the right way to implement this is something like how `IWindowManager` works for managing Windows. Some ideas towards that would be great. – JobaDiniz Feb 11 '15 at 13:03
  • I never said that you should implement the `IHandle` interface on a viewmodel. You could implement it on a behaviour that you attach to your TaskBarIcon. Publishing an event alone is useless, that's why you need to implement the `IHandle` interface and register the implementing class as a listener. – Roel van Westerop Feb 11 '15 at 13:09
  • Publishing events is not the way to do with. The example I gave was fairly simple, but there are other features of TaskBarIcon, like showing a [custom ballon](http://www.codeproject.com/Articles/36468/WPF-NotifyIcon#balloons) (instanciate, bind the appropriate viewmodel - which Caliburn does for us, just need to think how to tell it that what to bind to)... – JobaDiniz Feb 14 '15 at 14:39
  • I've update my entire question. Could you take a look? – JobaDiniz Feb 18 '15 at 12:45
  • Could it be possible you're creating the wrong object in your `TrayIconManager`? var icon = view is TaskbarIcon ? (TaskbarIcon)view : new TaskbarIcon(); – Roel van Westerop Feb 19 '15 at 08:51
  • The _view_ variable is of type **TrayIconView**, but _TrayIconView_ inherits from **TaskbarIcon**, so I just cast to _TaskbarIcon_ as I don't have knowledge of the actual type. – JobaDiniz Feb 21 '15 at 10:56
  • I have no idea why it's not working. When I attach an handler to the `TrayLeftMouseDown` event in the `TrayIconManager`, it's called. When I add a `TrayIconView` to the `ShellView` and add the method `ShowWindow` to the `ShellViewModel`, it's called. – Roel van Westerop Feb 23 '15 at 07:04
  • Thanks for helping... My handler is attached to the `TrayIconViewModel` and not `ShellViewModel`. Have your tests worked when attaching to the `TrayIconViewModel`? – JobaDiniz Feb 23 '15 at 11:00
  • No, thats why I tried attaching it to the `ShellViewModel`, with a `TrayIconView` in the `ShellView`. The debug logging from the `ViewModelBinder` shows it can't resolve the `ShowWindow`. Add this to your bootstrapper: var baseGetLog = LogManager.GetLog; LogManager.GetLog = t => t == typeof (ViewModelBinder) ? new DebugLog(t) : baseGetLog(t); – Roel van Westerop Feb 24 '15 at 06:00
  • I got `[Caliburn.Micro.ViewModelBinder] INFO: Action Convention Not Applied: No actionable element for ShowWindow.` – JobaDiniz Feb 25 '15 at 01:31
3

The complete answer:

ITrayIcon.cs

Wraps TaskbarIcon control, so you could call methods on TaskbarIcon without having an actual reference to it in your view models.

public interface ITrayIcon : IDisposable
{
    void Show();
    void Hide();
    void ShowBalloonTip(string title, string message);
    void ShowBalloonTip(object rootModel, PopupAnimation animation, TimeSpan? timeout = null);
    void CloseBalloon();
}

ISetTrayIconInstance.cs

public interface ISetTrayIconInstance
{
    ITrayIcon Icon { set; }
}

ITrayIconManager.cs

Manages TasbarIcon instances. You can have as many TasbarIcon instances as you like.

public interface ITrayIconManager
{
    ITrayIcon GetOrCreateFor<T>();
}

TrayIconWrapper.cs

The implementation leverages from Caliburn ViewModelBinder. ShowBallonTip works alike IWindowManager.ShowWindow(object rootModel...) does. It instanciates the view through ViewLocator, binds your rootModel to it and then passes to TaskbarIcon.ShowCustomBallon(UIElement element....

public class TrayIconWrapper : ITrayIcon
{
    private readonly TaskbarIcon icon;

    public TrayIconWrapper(TaskbarIcon icon)
    {
        this.icon = icon;
    }

    public bool IsDisposed { get; private set; }

    public void Dispose()
    {
        icon.Dispose();
        IsDisposed = true;
    }

    public void Show()
    {
        icon.Visibility = Visibility.Visible;
    }

    public void Hide()
    {
        icon.Visibility = Visibility.Collapsed;
    }

    public void ShowBalloonTip(string title, string message)
    {
        icon.ShowBalloonTip(title, message, BalloonIcon.Info);
    }

    public void ShowBalloonTip(object rootModel, PopupAnimation animation, TimeSpan? timeout = null)
    {
        var view = ViewLocator.LocateForModel(rootModel, null, null);
        ViewModelBinder.Bind(rootModel, view, null);

        icon.ShowCustomBalloon(view, animation, timeout.HasValue ? (int)timeout.Value.TotalMilliseconds : (int?)null);
    }

    public void CloseBalloon()
    {
        icon.CloseBalloon();
    }
}

TrayIconManager.cs

We need to keep track of the instanciated TaskbarIcon's. This class is the glue that binds all together. Need an instance of some TaskbarIcon? Ask to TrayIconManager, and it will create one (if it isn't already created and alive) and return it to you. The T generic type is the type of your view model that manages an instance of TaskbarIcon.

public class TrayIconManager : ITrayIconManager
{
    private readonly IDictionary<WeakReference, WeakReference> icons;

    public TrayIconManager()
    {
        icons = new Dictionary<WeakReference, WeakReference>();
    }

    public ITrayIcon GetOrCreateFor<T>()
    {
        if (!icons.Any(i => i.Key.IsAlive && typeof(T).IsAssignableFrom(i.Key.Target.GetType())))
            return Create<T>();

        var reference = icons.First(i => i.Key.IsAlive && typeof(T).IsAssignableFrom(i.Key.Target.GetType())).Value;
        if (!reference.IsAlive)
            return Create<T>();

        var wrapper = (TrayIconWrapper)reference.Target;
        if (wrapper.IsDisposed)
            return Create<T>();

        return wrapper;
    }

    private ITrayIcon Create<T>()
    {
        var rootModel = IoC.Get<T>();
        var view = ViewLocator.LocateForModel(rootModel, null, null);
        var icon = view is TaskbarIcon ? (TaskbarIcon)view : new TaskbarIcon();
        var wrapper = new TrayIconWrapper(icon);

        ViewModelBinder.Bind(rootModel, view, null);
        SetIconInstance(rootModel, wrapper);
        icons.Add(new WeakReference(rootModel), new WeakReference(wrapper));

        return wrapper;
    }

    private void SetIconInstance(object rootModel, ITrayIcon icon)
    {
        var instance = rootModel as ISetTrayIconInstance;
        if (instance != null)
            instance.Icon = icon;
    }
}

HOW TO USE:

  1. Create a View that inherits from TaskbarIcon:

TrayIconView.xaml

<tb:TaskbarIcon x:Class="Communicator.Softphone.Views.TrayIconView"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
            xmlns:tb="http://www.hardcodet.net/taskbar"
            xmlns:cal="http://www.caliburnproject.org"
            mc:Ignorable="d" 
            d:DesignHeight="300" d:DesignWidth="300"
            IconSource="/Communicator.ControlLibrary;component/Assets/phone_icon.ico"
            ToolTipText="Secretária do Futuro - Comunicador"
            cal:Message.Attach="[Event TrayLeftMouseDown] = [Action ShowWindow()]"
            TrayLeftMouseDown="TaskbarIcon_TrayLeftMouseDown">

  1. Create a view model for the view:

TrayIconViewModel.cs

public class TrayIconViewModel : ISetTrayIconInstance
{
    public TrayIconViewModel()
    {

    }

    public ITrayIcon Icon { get; set; }

    public void ShowWindow()
    {
        System.Windows.Application.Current.MainWindow.Show(); //very very bad :(
    }
}
  1. Instanciate it through ITrayIconManager in any place. For example, in the OnActivat method of your ShellViewModel:

    protected override void OnActivate()
    {
        trayIcon = trayIconManager.GetOrCreateFor<TrayIconViewModel>();
    }
    
  2. Use whenever you like. For example, in my ChatManager:

    public void NewMessage(IChatMessage message)
    {
        trayIcon = trayIconManager.GetOrCreateFor<TrayIconViewModel>();
        var notification = new ChatNotificationViewModel(message);
        trayIcon.ShowBalloonTip(notification, PopupAnimation.Slide, TimeSpan.FromSeconds(5));
    }
    
JobaDiniz
  • 862
  • 1
  • 14
  • 32