Introducción
En el artículo anterior,
repasamos las novedades a nivel empresarial disponibles con la llegada
de Windows Phone 8. Contamos con mejoras a nivel de servicios,
seguridad, encriptación y la distribución de Aplicaciones. Podemos
realizar una distribución no administrada que permite distribuir
aplicaciones de manera muy simple mediante un simple enlace o con un
archivo adjunto en un correo electrónico. También vimos un nuevo
concepto disponible con Windows Phone 8 llamado Company Hub que permite crear un Hub personalizado donde mostrar y permitir instalar las aplicaciones disponibles de la empresa.
Creando un Company Hub
Nuestro Company Hub constara de dos partes:
- Servidor.
Nuestro objetivo es un servidor que devuelve la meta información de las
aplicaciones junto a los Xaps de las mismas a la Aplicación Windows
Phone. Las posibilidades son múltiples, podemos utilizar blobs de Azure
para gestionar la meta información, servicio WCF, etc. En nuestro
ejemplo crearemos una aplicación WCF muy sencilla.
- Cliente.
Aplicación Windows Phone 8 que se nutre de la información del servidor
además de apoyarse en las APIs previamente mencionadas para permitir
gestionar las Aplicaciones (instalar, lanzar, etc.).
Comenzamos
nuestro Company Hub por la parte del servidor, ya que sin ella, no
podríamos avanzar.Creamos el proyecto para la aplicación WCF.
Seleccionamos la plantilla “WCF Service Application”:
Añadimos carpetas para incluir los recursos de las aplicaciones, Xaps, etc:
Debemos añadir los Xaps de nuestro Company Hub a la carpeta Xaps
pero… esperad!, no podemos copiar el resultado de la compilación de cada
Aplicación tal cual, debemos preparar las Aplicaciones.
¿Cómo?
Debemos precompilar y firmar las Aplicaciones con nuestro certificado
empresarial. Para hacerlo utilizaremos una herramienta llamada BuildMDILXap
que nos realiza ambas acciones. La herramienta la tenemos disponible
con la instalación del SDK de Windows Phone en la siguiente carpeta:
%ProgramFiles(x86)%Microsoft SDKsWindows Phonev8.0ToolsMDILXAPCompile
Abrimos PowerShell:
C:WindowsSystem32WindowsPowerShellv1.0
Nos dirigimos a la ruta anteriormente indicada donde tenemos disponible la herramienta MDILXAPCompile:
cd ‘C:Program Files (x86)Microsoft SDKsWindows Phonev8.0ToolsMDILXAPCompile’
Ejecutamos la siguiente línea:
.BuildMDILXap.ps1 –xapfilename
Donde:
- -xapfilename: Ruta compelta al xap que deseamos precompilar y firmar.
- -pfxfilename: Ruta al certificado Symantec que utilizaremos para firmar la Aplicación.
- -password: La contraseña del archivo PFX.
Por último, añadimos el archivo XML que contiene toda la meta información que necesitamos para gestionar las Aplicaciones:
<?xml version=
"1.0"
encoding=
"utf-8"
?>
<CompanyApp>
<ProductId>{957dc000-642f-45bc-a710-87ea0d33fee3}</ProductId>
<Title>App01</Title>
<Description>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
In
aliquam convallis elit, ac aliquam neque vulputate eget. Sed consequat non nibh quis pharetra. </Description>
<Version>2.0.0.0</Version>
</CompanyApp>
<CompanyApp>
<ProductId>{73096706-377f-4423-b7e0-914b3724dce2}</ProductId>
<Title>App02</Title>
<Description>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
In
aliquam convallis elit, ac aliquam neque vulputate eget. Sed consequat non nibh quis pharetra. </Description>
<Version>1.0.0.0</Version>
</CompanyApp>
</ArrayOfCompanyApp>
Necesitamos devolver la colección de Aplicaciones. Por lo tanto
necesitamos definir la información de cada Aplicación. Creamos la clase
CompanyApp:
[Serializable]
[DataContract]
public class CompanyApp
{
[DataMember]
public Guid ProductId { get; set; }
[DataMember]
public string Title { get; set; }
[DataMember]
public string Description { get; set; }
[DataMember]
public string Author { get; set; }
[DataMember]
public string Icon { get; set; }
[DataMember]
public string XapPath { get; set; }
[DataMember]
public bool Downloading { get; set; }
[DataMember]
public uint Progress { get; set; }
[DataMember]
public string ProgressString { get; set; }
[DataMember]
public Status Status { get; set; }
[DataMember]
public string Version { get; set; }
}
Por último, nuestro servicio, leera el archivo XML, lo parseará y devolverá el listado de objetos CompanyApp:
public List<CompanyApp> GetCompanyApps()
{
var appPath = System.Web.Hosting.HostingEnvironment.ApplicationPhysicalPath;
CompanyApp[] packages;
using (var sr = new StreamReader(appPath +
"/CompanyApps.xml"
))
{
var dcs = new XmlSerializer(typeof(CompanyApp[]), new[] { typeof(CompanyApp) });
packages = dcs.Deserialize(sr.BaseStream) as CompanyApp[];
}
return packages.ToList();
}
Nada más por parte del servidor. Nos centramos ya en la Aplicación Windows Phone. Creamos un nuevo proyecto:
Crearemos una Aplicación inicial de tipo Panorama donde mostraremos dos secciones:
- Un listado de las aplicaciones disponibles que podremos instalar.
- Un listado de las aplicaciones instaladas que podremos lanzar.
Por lo tanto la estructura de la misma será:
<phone:Panorama Title=
"company hub"
>
<!--
New
Apps-->
<phone:PanoramaItem Header=
"Nuevas"
>
<ctl:LongListSelector/>
</phone:PanoramaItem>
<!--Installed Apps-->
<phone:PanoramaItem Header=
"Instaladas"
>
<ctl:LongListSelector/>
</phone:PanoramaItem>
</phone:Panorama>
NOTA: Recordar que estamos ante un ejemplo práctico y
que podemos incluir en el Hub de la empresa mucha más información como
las aplicaciones que cuentan con actualización, un listado de noticias
de la empresa e incluso podemos añadir notificaciones push para avisar
con novedades.
Vamos a añadir la referencia al servicio WCF. Hacemos clic derecho
sobre las referencias del proyecto y seleccionamos “Añadir referencia de
servicio”.
Utilizaremos el patrón MVVM como estructura de
nuestra Aplicación. En la Vista-Modelo necesitaremos dos propiedades
donde almacenar las colecciones de Aplicaciones nuevas e instaladas:
private ObservableCollection<CompanyApp> _newApps;
private ObservableCollection<CompanyApp> _installedApps;
Creamos las propiedades:
public ObservableCollection<CompanyApp> NewApps
{
get { return _newApps; }
set { SetProperty(ref _newApps, value); }
}
public ObservableCollection<CompanyApp> InstalledApps
{
get { return _installedApps; }
set { SetProperty(ref _installedApps, value); }
}
Necesitamos un método que acceda al servicio WCF y llame al método del servicio que devuelve el listado de Aplicaciones:
private async Task<List<CompanyApp>> GetAppListAsync()
{
var service = new CompanyHubServerClient();
var tcs = new TaskCompletionSource<List<CompanyApp>>();
EventHandler<GetCompanyAppsCompletedEventArgs> handler = null;
handler = (s, ex) =>
{
service.GetCompanyAppsCompleted -= handler;
if (ex.
Error
!= null)
tcs.SetException(ex.
Error
);
else
tcs.SetResult(ex.Result.ToList());
};
service.GetCompanyAppsCompleted += handler;
service.GetCompanyAppsAsync();
return await tcs.Task;
}
Utilizaremos ese método para obtener el listado de Aplicaciones
disponibles en el servidor. Por otro lado, utilizaremos el método FindPackagesForCurrentPublisher
para obtener el listado de Aplicaciones del mismo publicador (nosotros
como empresa) instaladas. Compararemos ambos listados para determinar
que Aplicación es nueva y cual esta ya instalada:
private async Task LoadDataAsync()
{
// Obtenemos el listado de Apps del servidor
_packages = await GetAppListAsync();
// Obtenemos el listado de Apps instaladas
var installed = InstallationManager.FindPackagesForCurrentPublisher();
foreach (var pkg in _packages)
{
var qry = installed.Where(p => p.Id.ProductId.ToLower().Contains(pkg.ProductId.ToString().ToLower()));
if (qry.Any())
pkg.Status = Status.Installed;
else
pkg.Status = Status.
New
;
}
NewApps = new ObservableCollection<CompanyApp>(_packages.Where(p => p.Status == Status.
New
).ToList());
InstalledApps = new ObservableCollection<CompanyApp>(_packages.Where(p => p.Status == Status.Installed).ToList());
}
Llamaremos al método desde el constructor del Vista-Modelo:
new Action(async () =>
{
await LoadDataAsync();
}).Invoke();
Sólo nos falta enlazar los controles de nuestra Vista con las
propiedades de Aplicaciones del Vista-Modelo. Comenzamos creando el
DataTemplate de cada Apliación en la lista:
<DataTemplate x:Key=
"listCompanyAppTemplate"
>
<StackPanel Margin=
"0,-6,0,12"
Orientation=
"Horizontal"
>
<Image Source=
"{Binding Icon}"
Width=
"100"
/>
<TextBlock Text=
"{Binding Title}"
TextWrapping=
"Wrap"
VerticalAlignment=
"Center"
Style=
"{StaticResource PhoneTextExtraLargeStyle}"
FontSize=
"{StaticResource PhoneFontSizeExtraLarge}"
/>
</StackPanel>
</DataTemplate>
Enlazamos el control LongListSelector con la propiedad utilizando su propiedad ItemsSource:
<ctl:LongListSelector Margin=
"0,0,-22,0"
ItemsSource=
"{Binding NewApps}"
ItemTemplate=
"{StaticResource listCompanyAppTemplate}"
SelectedItem=
"{Binding SelectedApp, Mode=TwoWay}"
/>
Todo listo!. Pulsamos F5 y ejecutamos la aplicación en el emulador. Tras
ejecutarse la aplicación y esperar unos leves segundos (tiempo que
tardará el programa en llamar al servicio) y…
Error!. Si vemos los detalles del error:
The remote server returned an error: NotFound.
Ya sabemos porque. El emulador de Windows Phone 8 es una máquina
virtual en Hyper-V y la red de la misma es distinta a la de la máquina
de desarrollo. Vamos a modificar el archivo de configuración de IIS
Express que lo podéis encontrar en la ruta:
%USERPROFILE%<your user name>DocumentsIISExpressconfig
El nombre del fichero es applicationhost.config. Lo
abrimos y buscamos la sección <sites>. Buscamos el sitio que tenga
el nombre de nuestro servicio y nos fijamos en el apartado
<bindings>.
Tenemos un binding ya establecido a localhost que no podemos
eliminar. Por lo tanto, añadimos un segundo binding igual al ya creado
reemplazando localhost por la IP de la máquina de desarrollo.
Vamos a añadir también en el Firewall de Windows una excepción para
IIS Express en el puerto que estámos utilizando. Guardamos los cambios.
Cerramos Visual Studio y lo volvemos a ejecutar con privilegios de
Administrador (es necesario para registrar nuestra aplicación con un
servicio no alojado en localhost). Ejecutamos la aplicación y voilá!
¿Podemos mejorar la Aplicación?
Sin duda, ya hemos comentado que podemos añadir funcionalidad extra a
nuestra Aplicación como noticias, notificaciones, alertas, etc. Sin
embargo, hay una pequeña modificación que aporta un extra importante en
la gestión de Aplicaciones, las actualizaciones.
Creamos una nueva colección en nuestro Vista-Modelo:
private ObservableCollection<CompanyApp> _updateApps;
Con su correspondiente propiedad:
public ObservableCollection<CompanyApp> UpdateApps
{
get { return _updateApps; }
set { SetProperty(ref _updateApps, value); }
}
¿Qué nos hace falta?. Sencillamente, una forma de detectar cuando una
Aplicación tiene una versión superior. Crearemos un Helper que nos
permita detectar cuando una Aplicación cuenta con una versión superior:
public static bool IsAnUpdate(string newVersion, string oldVersion)
{
var newVer = System.Version.Parse(newVersion);
var oldVer = System.Version.Parse(oldVersion);
return newVer.CompareTo(oldVer) > 0;
}
Cuando recorremos el listado de Aplicaciones utilizamos nuestro Helper
para determinar si una Aplicación es nueva o una actualización:
pkg.Status = CompanyAppHelper.IsAnUpdate(
pkg.Version, devicePkgVersion) ?
Status.Update : Status.Installed;
Añadimos en nuestra interfaz un tercer PanoramaItem para mostrar el listado de actualizaciones:
<!--Update Apps-->
<phone:PanoramaItem Header=
"Actualizaciones"
>
<ctl:LongListSelector Margin=
"0,0,-22,0"
ItemsSource=
"{Binding UpdateApps}"
ItemTemplate=
"{StaticResource listCompanyAppTemplate}"
SelectedItem=
"{Binding SelectedApp, Mode=TwoWay}"
/>
</phone:PanoramaItem>
Todo listo!. Si ejecutamos ahora veremos el tercer PanoramaItem con
las actualizaciones por lo que ya tenemos nuestro Company Hub
permitiendonos realizar una gestión completa de las Aplicaciones.
¿De verdad? Pero… si nisiquiera podemos instalar Aplicaciones…
Esto,.. tenéis razón, vamos a remediarlo. Empezaremos añadiendo una
nueva Vista que mostrará los detalles de una Aplicación seleccionada
donde permitiremos instalar o lanzar la Aplicación además de por
supuesto mostrar los detalles de la misma.
La lógica de esta segunda vista deberá ir en un nuevo vista modelo
donde lo principal será la Aplicación seleccionada. Añadiremos por lo
tanto una variable de tipo CompanyApp en el vista modelo:
private CompanyApp _currentApp;
Con su correspondiente propiedad pública:
public CompanyApp CurrentApp
{
get { return _currentApp; }
set
{
SetProperty(ref _currentApp, value);
}
}
En la vista mostraremos los detalles principales de la Aplicación, en título, la descripción y la versión:
<StackPanel Orientation=
"Vertical"
Margin=
"20,0,0,0"
>
<TextBlock Text=
"VERSION"
Style=
"{StaticResource PhoneTextSubtleStyle}"
FontSize=
"{StaticResource PhoneFontSizeMediumLarge}"
Margin=
"0,20,0,0"
/>
<TextBlock Text=
"{Binding CurrentApp.Version}"
TextWrapping=
"Wrap"
Style=
"{StaticResource PhoneTextLargeStyle}"
FontSize=
"{StaticResource PhoneFontSizeLarge}"
/>
<TextBlock Text=
"DESCRIPTION"
Style=
"{StaticResource PhoneTextSubtleStyle}"
FontSize=
"{StaticResource PhoneFontSizeMediumLarge}"
Margin=
"0,20,0,0"
/>
<TextBlock Text=
"{Binding CurrentApp.Description}"
TextWrapping=
"Wrap"
Style=
"{StaticResource PhoneTextLargeStyle}"
FontSize=
"{StaticResource PhoneFontSizeLarge}"
/>
</StackPanel>
Podríamos hacer la vista mucho más compleja y similar a la Store con un
Panorama con múltiples secciones, imágenes de la Aplicación e incluso
Aplicaciones relacionadas. Para nuestro caso lo dejaremos en algo más
simple. Lo que si debemos permitir es al menos, instalar y lanzar
Aplicaciones. Para ello vamos a añadir un par de botones :
<Button Content=
"Install
"
/>
<Button Content=
"Launch
"
/>
Debemos añadir los comandos correspondientes para cada botón:
private ICommand _installCommand;
private ICommand _launchCommand;
Enlazamos la interfaz con cada comando:
<Button Content=
"Install"
Command=
"{Binding InstallCommand}"
/>
<Button Content=
"Launch"
Command=
"{Binding LaunchCommand}"
/>
Nos centramos de entrada en la lógica del comando que utilizaremos para
instalar Aplicaciones. Verificamos que el fichero de la Aplicación
existe y en caso afirmativo llamamos al método AddPackageAsync que procederá con la instalación de la Aplicación:
if (!string.IsNullOrEmpty(CurrentApp.XapPath))
{
var result = await InstallationManager.AddPackageAsync(CurrentApp.Title, new Uri(CurrentApp.XapPath));
if (result.InstallState == Windows.Management.Deployment.PackageInstallState.Installed)
_dialogService.Show(CurrentApp.Title +
" ha sido instalada."
);
}
En el segundo comando, que nos permite lanzar una Aplicación, primero
obtendremos el listado de Aplicaciones instaladas para posteriormente
intentar lanzar la Aplicación deseada con el método Launch:
var devicePkgs = InstallationManager.FindPackagesForCurrentPublisher();
var devicePkg = devicePkgs.FirstOrDefault(p => p.Id.ProductId.Contains(CurrentApp.ProductId.ToString().ToUpper()));
if (devicePkg != null)
devicePkg.Launch(string.Empty);
Puedes descargar el ejemplo del Company Hub a continuación:
Conclusiones
Las opciones disponibles para poder distribuir aplicaciones a nivel
empresarial crecen y es algo sumamente positivo. A los sistemas MDM u
otras alternativas como publicar en la Store la Aplicación protegiendola
con credenciales se suma la opción vista en este artículo donde cabe
destacar la sencillez con la que podemos distribuir las aplicaciones,
basta con un simple email o un enlace, además de ser una opción bastante
asequible para la empresa. Recordando los pasos vistos:
- Crear una cuenta de desarrollo para Windows phone a nivel de empresa.
- Obtener el certificado de Symantec.
- Generar el archivo AET.
- Firmar las Aplicaciones que queremos distribuir.
- Opcionalmente, crear un Company Hub para facilitar a empleados la gestión de Aplicaciones.
Más Información