Chronos v1.0 RC 1 disponible en codeplex


La descarga está disponible aquí.

Anuncios

chronos: escritorios virtuales


Aprovechando los cambios que he estado haciendo la última seana para replantear una parte de chronos (tengo pendiente escribir un post sobre el tema), estos dos últimos dias he aprovechado para implementar los escritorios virtuales (bueno va a dar soporte a dos escritorios solamente por el momento).

El diagrama de clases para los escritorios virtuales queda así (por el momento)

image

En la ventana principal los escritorios se registran como escritorios virtuales así

<desktopControls:Desktop
        x:Name="VirtualDesktop01"
        Grid.Row="3"
        Id="94fa801f-bef7-437a-ba75-352f1ede6fb9"
        Visibility="Collapsed"
        windows:VirtualDesktopManager.IsDesktop="True"
        dragdrop:DragDropManager.IsDropTarget="True"
        Background="Transparent"
        AllowDrop="True"
        SnapsToDevicePixels="True"
        ClipToBounds="False">
</desktopControls:Desktop>

<desktopControls:Desktop
        x:Name="VirtualDesktop02"
        Grid.Row="3"
        Id="2afc760e-9450-448d-9f28-5311c49e4773"
        Visibility="Collapsed"
        windows:VirtualDesktopManager.IsDesktop="True"
        dragdrop:DragDropManager.IsDropTarget="True"
        Background="Transparent"
        AllowDrop="True"
        SnapsToDevicePixels="True"
        ClipToBounds="False">
</desktopControls:Desktop>

El Id servirá para identificar, internamente, una instancia concreta de un escritorio y el nombre se utilizará para el archivo que se generará al guardar el escritorio.

La “dependency property” VirtualDesktopManager.IsDesktop se

encargará de registrar el escritorio virtual.

El ViewModel de la ventana principal tiene un nuevo comando para cambiar de escritorio

public ICommand SwitchDesktopCommand
{
    get
    {
        if (this.switchDesktop == null)
        {
            this.switchDesktop = new ActionCommand(() => OnSwitchDesktop());
        }

        return this.switchDesktop;
    }
}

que se encargará de realizar el cambio de la siguiente forma

/// <summary>
/// Handles the switch desktop command action
/// </summary>
private void OnSwitchDesktop()
{
    ServiceLocator.GetService<IVirtualDesktopManager>().SwitchDesktop();
}

La clase VirtualDesktopManager está declara como un servicio de nRoute


[MapService(typeof(IVirtualDesktopManager),
    InitializationMode=InitializationMode.WhenAvaliable,
    Lifetime=InstanceLifetime.Singleton)]
public sealed class VirtualDesktopManager : ObservableObject, IVirtualDesktopManager

tiene métodos para, cambiar de escritorio, activar el escritorio por defecto (que por el momento será siempre el primero registrado), visualizar una ventana, widget o acceso directo en el escritorio activo, etc. En general los métodos de esta clase se limitan a llamar a los métodos equivalentes de la clase que representa el escritorio virtual (que debe ser una instancia de VirtualDesktop).

Ahí va un video mostrando el cambio de escritorio (está grabado con camstudio, la calidad no es muy buena … pero como ejemplo debería ser más que suficiente)

chronos: Implementando una ventana de login


Chronos tenía hasta ahora una opción para cerrar la sesión de usuario activo, que no hace nada ahora mismo, y un label en el que se debería mostrar el usuario activo pero que al no haber funcionalidad para la identificación del usuario muestra siempre “Usuario anónimo”, así que me he puesto manos a la obra para implementar la identificación del usuario.

Para la implementación he utilizado un observador dedicado de nRoute, y lógicamente una ventana modal para pedir el nombre y la contraseña del usuario, además el ViewModel del Shell (ShellViewModel) tendrá su propio observador para poder actualizar el nombre del Usuario que se visualiza en pantalla.

image 

La ventana de login se abre una vez que la aplicación se ha iniciado, así

// Publish LogOn Request 
Channel<AuthenticationInfo>.Manager.PublishAsync(
    new AuthenticationInfo
    {
        Action = AuthenticationAction.LogOn
    });

 

En AuthenticationObserver se evaluará la acción a realizar en función de los datos que le lleguen

switch (value.Action)
{
    case AuthenticationAction.LogOn:
        ServiceLocator.GetService<INavigationService>().NavigateTo("Chronos/LogIn");
        break;

    case AuthenticationAction.LoggedIn:
        ServiceLocator.GetService<IElementManager>().Load("Desktop.xaml");
        break;

    case AuthenticationAction.LogOut:
        ThreadPool.QueueUserWorkItem(
            new WaitCallback(
                delegate
                {
                    ServiceLocator.GetService<IWindowManager>().CloseAllWindows();
                    ServiceLocator.GetService<IElementManager>().CloseAllElements();
                    Channel<AuthenticationInfo>.Manager.PublishAsync(
                        new AuthenticationInfo { Action = AuthenticationAction.LogOn });
                }));
        break;
}

Cuando la acción sea LogOn, se abrirá la ventana de identificación de usuario, cuando sea LoggedIn, se intentará realizar la carga del escritorio guardado en disco y cuando se LogOut cerrará todas las ventanas, los widgets, los accesos directos del escritorio y notificará de nuevo la acción de LogOn.

El observador de ShellViewModel está definido así

// create the observer
this.authenticationObserver = new ChannelObserver<AuthenticationInfo>(
    (l) => OnAuthenticationAction(l));

// subscribe on the UI Thread
this.authenticationObserver.Subscribe(ThreadOption.UIThread);

 

y se encargará simplemente de actualizar el nombre de usuario dependiendo de si la acción notificada es LogOn o LogOut

private void OnAuthenticationAction(AuthenticationInfo info)
{
    switch (info.Action)
    {
        case AuthenticationAction.LoggedIn:
            this.UserName = info.UserName;
            break;

        case AuthenticationAction.LogOut:
            this.UserName = null;
            break;

    }
}
 
image 
 

Ahora mismo no se realiza ninguna validación sobre los datos de identificación introducidos salvo que estos no estén vacios, en el momento en el que el usuario acepta los datos se notifica la acción de LoggedIn, con el nombre del usuario y se cierra la ventana

private void OnAccept()
{
    Channel<AuthenticationInfo>.Manager.PublishAsync(
        new AuthenticationInfo 
        { 
            Action = AuthenticationAction.LoggedIn, 
            UserName = this.UserName 
        });

    ServiceLocator.GetService<IWindowManager>().CloseDialog();
}  

Y por último el LogOut se notifica en la acción asociada a la opción de cerrar sesión del Shell

protected virtual void OnCloseSession()
{
    Channel<AuthenticationInfo>.Manager.PublishAsync(
        new AuthenticationInfo { Action = AuthenticationAction.LogOut });
}

 
image 

Ahí va un pantallazo de la nueva ventana de login

image

Chronos: Jugando con la barra de tareas de Windows 7


Hacía tiempo que quería ver un poco como funciona la integración desde una aplicación de WPF con la barra de tareas de Windows 7, así que para probar un poco como funciona he implementado una lista de vista abiertas recientemente en Chronos. Para la integración con la barra de tareas de Windows 7 he utilizado las clases que están en el namespace System.Windows.Shell (de .NET 4.0).

Aprovechando le he echado un vistazo también al “Messaging Broker” que viene con nRoute (que me parece muy muy interesante).

Como lo que se pretende es generar una lista de las operativas abiertas recientemente, lo que he hecho has sido modificar el servicio de navegación para que publique un mensaje en un canal a través del sistema de mensajeria de nRoute

// Publish the navigation message
Channel<NavigationInfo>.Manager.PublishAsync(
new NavigationInfo
(
response.Request.Url,
response.NavigationStateManager.Title
));

La clase NavigationInfo nos servirá como “payload” del mensaje para notificar la URL de la vista y un texto para visualizar en la JumpList

public sealed class NavigationInfo
{
    #region · Fields ·

    private string navigationUri;
    private string displayName;

    #endregion

    #region · Properties ·

    public string NavigationUri
    {
        get { return this.navigationUri; }
    }

    public string DisplayName
    {
        get { return this.displayName; }
    }

    #endregion

    #region · Constructors ·

    internal NavigationInfo(string navigationUri, string displayName)
    {
        this.navigationUri  = navigationUri;
        this.displayName    = displayName;
    }

    #endregion
}

Para recibir el mensaje he implementado un suscriptor dedicado de nRoute, NavigationObserver

[MapChannelObserver(typeof(NavigationInfo),
    InitializationMode = InitializationMode.WhenAvaliable,
    Lifetime = InstanceLifetime.Singleton,
    ThreadOption = ThreadOption.UIThread)]
public sealed class NavigationObserver : nRoute.Components.IObserver<NavigationInfo>

que se encargará de comprobar si la plataforma en la que está ejecutándose la aplicación es Windows 7 y en tal caso añadir la vista a una JumpList personalizada (en el caso de que no exista)

if (App.RunningOnWin7)
{
    JumpList jl = JumpList.GetJumpList(Application.Current);

    if (jl != null)
    {
        if (jl.JumpItems.Count >= 10)
        {
            jl.JumpItems.Clear();
        }

        var q = jl.JumpItems.OfType<JumpTask>().Where(t => t.Arguments.Equals(value.NavigationUri));

        if (q.Count() == 0)
        {
            JumpList.GetJumpList(Application.Current).JumpItems.Add
            (
                new JumpTask
                {
                    CustomCategory      = "Reciente",
                    Title               = value.DisplayName,
                    Arguments           = value.NavigationUri,
                    IconResourcePath    = null,
                    IconResourceIndex   = -1,
                    Description         = null,
                    WorkingDirectory    =
                        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
                }
            );

            jl.Apply();
        }
    }
}

de los posts en blogs que he visto sobre la barra de tareas de Windows 7 todos (o casi) tienen como ejemplo de las JumpList la ejecución de aplicaciones externas, que es el caso más sencillo, en este caso lo que se necesita es que la aplicación capture la acción que el usuario hace sobre uno de los elementos de la JumpList y abra la vista correspondiente.

El mejor ejemplo de como hacerlo lo he visto en la aplicación Fishbowl, en su clase SingleInstance, utilizan Remoting y IPC para conseguirlo.

Y aquí va un pantallazo de como se ve la JumpList

image

Chronos: Ventanas modales y MessageWindowElement


He hecho algunos cambios (que subí ayer a Codeplex) en Chronos para que las ventanas modales se comporten realmente como tal y para la visualización de cuadros de dialogo de confirmación personalizados (el típico MessageBox).

Hasta ahora las ventanas modales en Chronos impedían hacer algo (usar el ratón por ejemplo ) fuera de la ventana modal, pero no paraban la ejecución a la espera de la respuesta de la finalización de la ventana modal.

Ahora el método WindowElement.ShowDialog devuelve un valor del tipo DialogResult:

image

y para la ejecución del hilo de la UI mediante el inicio de un bucle de mensajes utilizando DispatcherFrame:

/// <summary>
/// Shows the window as a modal dialog
/// </summary>
public DialogResult ShowDialog()
{
    this.Parent     = WindowElement.ModalContainerPanel;
    this.IsModal    = true;

    this.Show();

    try
    {
        // Set DialogResult default value
        this.DialogResult = Services.DialogResult.None;

        // Create a DispatcherFrame instance and use it to start a message loop
        this.dispatcherFrame = new DispatcherFrame();
        Dispatcher.PushFrame(this.dispatcherFrame);
    }
    finally
    {
    }

    return this.DialogResult;
}

Además he implementado un nuevo elemento de escritorio MessageWindowElement (que hereda de WindowElement) que permite visualizar un típico MessageBox pero con un estilo personalizado:

image

La visualización del cuadro de dialogo se realiza mediante la implementación de un ViewService de nRoute:

image

 

Por ejemplo, la visualización del mensaje de confirmación de borrado de un acceso directo está implementada así:

IShowMessageViewService showMessageService = ViewServiceLocator.GetViewService<IShowMessageViewService>();

showMessageService.ButtonSetup  = DialogButton.YesNo;
showMessageService.Caption      = "Aviso";
showMessageService.Text         = "¿Está seguro de que desea eliminar el acceso directo?";

if (showMessageService.ShowMessage() == DialogResult.Yes)
{
    ServiceLocator.GetService<IElementManager>().CloseElement(this.Id);
}

 

Ahí va un pantallazo de como se ve el cuadro de dialogo:

image

Navegaciones con nRoute (II)


Sigo revisando la recomendación que me hizo el autor de nRoute de utilizar el atributo MapViewModelViewNavigationAttribute para así sacar partido de implementar la interface ISupportNavigationState en el ViewModel de las vistas

Para ello he implementado la interface ISupportNavigationState en la clase base para la implementación de ViewModel de las vistas, de momento son simplemente un a serie de métodos que será posible sobreescribir en aquellos ViewModels que lo necesiten.

public string Title
{
    get { return this.DisplayName; }
}

public void InitializeState(ParametersDictionary state)
{
    this.OnInitializeState(state);
}

public void RestoreState(ParametersDictionary state)
{
    this.OnRestoreState(state);
}

public ParametersDictionary SaveState()
{
    return this.OnSaveState();
}

protected virtual void OnInitializeState(ParametersDictionary state)
{
}

protected virtual void OnRestoreState(ParametersDictionary state)
{
}

protected virtual ParametersDictionary OnSaveState()
{
    return null;
}

 

Además he modificado el servicio de Navegacion de Chronos para que implemente la interface INavigationHandler de nRoute (lo he planteado así por que no estoy usando, al menos por el momento, los contenedores de navegación que implementa nRoute), de forma que sea posible validar el la petición de navegación realizada a nRoute y procesar la respuesta con el resultado devuelto por nRoute al resolver la nevegación, la forma de procesa la respuesta es casi la misma que la que se utiliza en uno de los contenedores de nRoute, en donde además se comprobará si la respuesta incluye una instancia de ISupportNavigationState que permita inicializar el estado de la vista después de haberla visualizado.

public bool ValidateRequest(NavigationRequest request)
{
    // basic check
    if (request == null)
    {
        throw new ArgumentNullException("request");
    }

    // by default
    return true;
}

public void ProcessResponse(NavigationResponse response)
{
    // basic check
    if (response == null)
    {
        throw new ArgumentNullException("response");
    }

    // we check if the navigation was successfull or not
    if (response.Status != ResponseStatus.Success)
    {
        // we inform the error 
        if (response.Status != ResponseStatus.Cancelled)
        {
            // ShowFailedVisualState(response.Request, response.Status, response.Error);
        }

        return;
    }

    // Show the window
    ServiceLocator.GetService<IWindowManager>().ShowWindow(response.Content as UIElement);

    // we initialize the view, if it supports it
    ISupportNavigationState navigationStateManager = response.NavigationStateManager;
    if (navigationStateManager != null)
    {
        navigationStateManager.InitializeState(response.ResponseParameters);
    }
}

 

y he modificado el método que se encargaba de resolver la navegación, para que utilice el método Navigate en lugar del método Resolve del servicio de navegación de nRoute (nRoute.Navigation.NavigationService)

public void NavigateTo(string target)
{
    Application.Current.Dispatcher.BeginInvoke(
        DispatcherPriority.Normal,
        new ThreadStart(delegate
            {
                nRoute.Navigation.NavigationService.Navigate(
                    new nRoute.Navigation.NavigationRequest(target, nRoute.Navigation.NavigateMode.New),
                    this);
            }));
}

Navegaciones con nRoute


El autor de nRoute me envió un mail hace un par de semanas (he andado enfermo y no he hecho nada estas dos últimas semanas) de una forma distinta de mapear las relaciones entre las vistas y los ViewModels que hace innecesario el uso del Behavior BridgeViewModelBehavior, para ello habría que usar el atributo MapViewModelViewNavigationAttribute, por ejemplo:

[MapView(typeof(EmpresasViewModel))]
[MapViewModelViewNavigationAttribute(NavigationUrl.Empresas, typeof(EmpresaView), ViewModelType=typeof(EmpresasViewModel))]
public partial class EmpresaView : WindowElement
{
    public EmpresaView()
    {
        InitializeComponent();
    }
}

Mucho más sencillo y funciona exactamente igual de bien (y puede tener funcionalidad extra si el ViewModel implementa ISupportNavigationState, aunque todavía no he podido verlo, a ver si a lo largo de la semana tengo un poco de tiempo y miro que ventajas tendría ).