Editar documentos almacenados como array de bits en SQL Server [FileStream] (3/n)
Es viernes, así que intentaré terminar la serie. Espero que no me quede un post muy ‘tocho’ 😛
Después de abrir boca con los dos posts anteriores, en los que hemos mostrado cómo crear una tabla con almacenamiento FILESTREAM, y posrteriormente cómo almacenar en ella documentos en forma de información binaria, hoy vamos a terminar la serie viendo cómo poder visualizar esta información mediante su aplicación asociada (Word, Excel, Acrobar Reader, etc.) y cómo no, editarla para guardar los cambios otra vez en la base de datos.
Mis disculpas 🙂
Antes de continuar os quiero decir que mi intención original era encontrar una solución más elegante, pero como no he sido capaz de encontrarla (y por lo que he visto en Internet, no he sido el único) os dejo una solución que no es tan elegante, pero al menos funciona.
Que cual era mi intención original? Pues me hubiese gustado poder abrir directamente la información binaria en -por ejemplo- Word. Creía que lo podría conseguir obteniendo un handle y pasándoselo a la aplicación asociada… pero los handles obtenidos mediante OpenSqlFilestream son locales al proceso 🙁
Dicho de otro modo, podemos acceder al fichero, leerlo, editarlo, pero sólo si lo hace nuestra aplicación. Si el fichero debe ser controlado a través de otra aplicación no podemos pasarle el handle. Así que esta opción no sirve.
Otra opción que contemplé (inspirado en mi querido y odiado SharePoint) fue crear un manejador HTTP con ASP.NET, ya que algunas aplicaciones son capaces de trabajar con documentos abiertos a través de una URL. Sin embargo, sólo conseguí hacerlo funcionar con documentos de Office (y no en el 100% de los casos).
La solución. Mi solución (seguro que hay más)
Así pues tuve que recurrir a la opción que pretendía evitar a toda costa, que no es otra que: Leer la información binaria, crear un fichero temporal en la estación cliente y abrirlo con la aplicación asociada. Que tiene de malo esta solución? Pues que para visualizar documentos funciona muy bien, pero en realidad estamos mostrando –y editando- una copia del documento, de modo que para revertir los cambios que hace el usuario a la base de datos tenemos un problema. Un problemón.
Después de darle algunas vueltas y comentarlo con más gente (gracias Pablo Gavela!) al final me decanté por utilizar la clase Process. Si, la misma clase process que hemos usado miles de veces para ejecutar aplicaciones o mostrar documentos. La clase process puede lanzar un proceso de forma síncrona o asíncrona.
La primera la deseché en seguida porque creo que usa algo que los viejos programadores del API de Win32 aprendimos a temer: WaitForExit (que llama a WaitForSingleObject). Además, la aplicación deja de responder hasta que se termina el proceso lanzado, con lo que ni siquiera repinta la ventana.
La segunda permite lanzar un proceso y monitorizar el momento en que se cierra mediante el evento ‘Exited’. De modo que podemos saber el momento en que se cierra la aplicación asociada, verificar si se ha cambiado el documento y en caso afirmativo volver a guardarlo en la base de datos.
Nota importante: Si queremos que se dispare el evento ‘Exited’ hay que activar la propiedad ‘EnableRaisingEvents’.
Un poco rocambolesco? Tal vez. Así que si alguien encuentra una solución más sencilla que lo haga público. Por favor 🙂
Al fin! El maldito código
Mi propuesta es crear una clase derivada de Process con la información adicional que necesitamos para el manejo de los datos binarios. En este caso particular necesito que el proceso ‘conozca’ el identificador del registro (FileId Guid), el nombre original del documento (o al menos su extensión), y evidentemente los propios datos binarios.
La idea es pasarle estos datos ‘extra’ en el constructor, y posteriormente llamar a un método que se encargue de crear el fichero temporal y obtener la fecha de la última escritura. Posteriormente ‘escucharemos’ el evento ‘Exited’ y cuando se dispare verificaremos si hay cambios (comparando la fecha de última escritura con la anterior) y si los hay, guardaremos los cambios en la base de datos a través del FileId que hemos pasado enteriormente al proceso.
La clase ProcessController
public class ProcessController : Process
{
DateTime originalLastWriteTime;
Byte[] document = null;
string tempfileName = string.Empty;
public string TempFileName
{
get
{
return tempfileName;
}
}
Guid fileId;
public Guid FileId
{
get
{
return fileId;
}
}
public ProcessController(Byte[] documentBytes, Guid docId, string originalfilename)
: base()
{
tempfileName = Path.GetTempFileName().Replace("tmp", getExtension(originalfilename));
document = documentBytes;
fileId = docId;
this.StartInfo.FileName = tempfileName;
}
public void EditInAssociatedApplication()
{
if (tempfileName == null) return;
if (File.Exists(tempfileName)) File.Delete(tempfileName);
File.WriteAllBytes(tempfileName, document);
originalLastWriteTime = File.GetLastWriteTime(tempfileName);
this.EnableRaisingEvents = true;
this.Start();
}
public bool HasChanged()
{
if (tempfileName == null) return false;
var modifiedtime = File.GetLastWriteTime(tempfileName);
return (modifiedtime != originalLastWriteTime);
}
private string getExtension(string filename)
{
var parts = filename.Split('.');
if (parts.Length > 0)
return parts[parts.Length - 1];
else
return string.Empty;
}
}
Para usar esta clase en nuestra aplicación lo mejor es tener una lista con los procesos que vamos abriendo, y por cada uno de ellos escuchar su evento ‘Exited’. De este modo cada vez que abrimos un proceso nuevo lo agregamos a la lista, y cada vez que se cierra un proceso, comprobamos si hay cambios y lo volcamos a la base de datos. Un ejemplo:
List<ProcessController> Processes = new List<ProcessController>();
private void editDocument(Guid docId)
{
var filestreamDoc = context.FileStreamDocuments.FirstOrDefault(
p => p.FileId == docId);
if (filestreamDoc == null) return;
ProcessController process = new ProcessController(
filestreamDoc.Document.ToArray(),
filestreamDoc.FileId, filestreamDoc.OriginalPath);
process.Exited += process_Exited;
process.EditInAssociatedApplication();
Processes.Add(process);
}
void process_Exited(object sender, EventArgs e)
{
ProcessController process = sender as ProcessController;
if (process == null) return;
if (process.HasChanged())
{
saveDocument(process.FileId, process.TempFileName);
}
}
private void saveDocument(Guid docId, string filename)
{
var filestreamDoc = context.FileStreamDocuments.FirstOrDefault(
p => p.FileId == docId);
if (filestreamDoc == null) return;
if (File.Exists(filename))
{
filestreamDoc.Document = File.ReadAllBytes(filename);
context.SubmitChanges();
}
}
No quiero alargar más el post, así que os dejo algo como ejercicio por si alguien se anima:
- Cada vez que se cierra un proceso deberíamos eliminarlo de la lista.
- Si el usuario cierra la aplicación y existen procesos abiertos, deberíamos avisarlo.
Ala, ya doy por acabada la serie. Y justo a tiempo. Nos leemos 🙂
3 Responsesso far
Muy buena la serie, relacionado con este post: http://geeks.ms/blogs/jirigoyen/archive/2010/03/25/constr-250-yete-tu-propio-gestor-documental.aspx
Otra forma de saber si un archivo se modifico es mediante la utilización del hash (obtener el hash antes entregar el doc al usuario y luego generarlo de nuevo cuando se va a guardar y comparar)
Salu2
Me alegro que te haya gustado!
Gracias por el link 😉
Excelente serié, creo además que es mucho mas adecuado para un gestor documental utilizar tu solución con Filestream pues si los archivos alojados son muy grandes la base de datos puede llegar a ser poco manejable.
El problema de que al editar se tenga que grabar el archivo de nuevo a disco, nosotros hemos tenido algún problema con los de autocad, algunos de mas de 20 gigas que tardaban bastante, hemos tenido que realizar llamadas asíncronas para no bloquear el programa, una de las ventajas es que fácilmente puedes implementar un control de versiones.
Seguramente el rendimiento sea menor en la búsqueda de contenidos indizados, aunque habría que probarlo.
Otra solución para aquellos que tengan Sharepoint es guardar y acceder a los documentos desde la aplicación utilizando un ws que ataque a Sharepoint, nosotros lo implementamos hace varios años y la verdad es que funciono muy bien.
Un saludo.