V minulém díle tohoto seriálu jsme si vysvětlili a podrobně popsali princip fungování MetaWeblog API, v tomto díle si napíšeme velice jednoduchý blog v ASP.NET, jehož obsah zpřístupníme pomocí tohoto systému. Nebudeme dělat žádné administrační rozhraní, to si může udělat každý sám, napíšeme jen čisté jádro. Každý si pak může aplikaci přizpůsobit k obrazu svému. Protože se chci vejít do jednoho článku, předpokládám od vás alespoň základní znalosti ASP.NET, jazyka SQL a Visual Basic .NET, bez toho se daleko nedostaneme. Pokud preferujete jazyk C#, vězte, že se není třeba ničeho obávat, Visual Basic má akorát jinou syntaxi, jinak je vše stejné. Pokud jste líní kód přepisovat, můžete využít konvertor.
Vytvoření databáze
Pro vytvoření databáze budete potřebovat nějaký program na správu MS SQL Serveru 2005 (doporučuji SQL Server Management Studio). Vytvořte si novou databázi (v tomto článku je její název SimpleBlog, pokud dáte název jiný, musíte jej na příslušných místech změnit) a spusťte na ní tyto příkazy, které vytvoří velmi jednoduchou strukturu:
CREATE TABLE [Articles] (
[ArticleId] INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[PublishedDate] DATETIME NOT NULL DEFAULT GETDATE(),
[Title] VARCHAR(200) NOT NULL,
[Html] TEXT NOT NULL,
[Visible] BIT NOT NULL
)
CREATE TABLE [Categories] (
[CategoryId] INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[Title] VARCHAR(200) NOT NULL
)
INSERT INTO [Categories] ([Title]) VALUES ('Databáze')
INSERT INTO [Categories] ([Title]) VALUES ('C#')
INSERT INTO [Categories] ([Title]) VALUES ('VB.NET')
INSERT INTO [Categories] ([Title]) VALUES ('Ostatní')
CREATE TABLE [ArticleCategories] (
[ArticleCategoryId] INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[ArticleId] INT NOT NULL REFERENCES [Articles] ([ArticleId]) ON DELETE CASCADE,
[CategoryId] INT NOT NULL REFERENCES [Categories] ([CategoryId]) ON DELETE CASCADE
)
Vytvořili jsme si tabulku Articles, která obsahuje články, tabulku Categories, která obsahuje kategorie, a vazebnou tabulku ArticleCategories, která obsahuje propojení článků s kategoriemi. Jeden článek je možné přiřadit do více kategorií, tabulka má nastaveno automatické odstranění záznamů v případě smazání článku nebo smazání kategorie.
Vytvoření ASP.NET WebSite
Vytvořte si novou ASP.NET WebSite (potřebujete Visual Studio 2005 nebo Visual Web Developer, který je zdarma) a odstraňte z ní soubor Default.aspx. Předpokládám, že budeme časem tomuto blogu přidávat nějaký vzhled, proto do projektu přidejte novou položku MasterPage a po vytvoření na ní pravým tlačítkem klikněte a zvolte Add Content Page. Tím se nám stránka Default.aspx vytvoří znovu, bude ale zaintegrována do MasterPage, takže ji můžeme jednoduše začlenit do layoutu stránky.
Otevřete editor kódu, vše z něj vymažte a vložte tam tento:
<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" title="SimpleBlog" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:SimpleBlogCS %>"
SelectCommand="SELECT [ArticleId], [PublishedDate], [Title] FROM [Articles] ORDER BY [PublishedDate] DESC"></asp:SqlDataSource>
<br />
<asp:GridView ID="GridView1" runat="server" DataSourceID="SqlDataSource1" AutoGenerateColumns="false">
<Columns>
<asp:HyperLinkField DataTextField="Title" HeaderText="Článek" DataNavigateUrlFields="ArticleId" DataNavigateUrlFormatString="Article.aspx?id={0}" ItemStyle-Font-Bold="true" />
<asp:BoundField DataField="PublishedDate" HeaderText="Publikováno" HtmlEncode="false" DataFormatString="{0:D}" ItemStyle-Width="20%" />
</Columns>
</asp:GridView>
</asp:Content>
GridView teď bude mít dva sloupce, první bude obsahovat název článku a bude odkazovat na adresu Article.aspx?id={0}, kde za {0} se doplní ID článku. Druhý sloupeček bude obsahovat datum publikování článku.
Tím jsme zvládli úplně základní výpis článků. Samozřejmě je na místě, abyste si změnili vzhled aplikace, vytvořili téma, ale o tom tento článek není. To, co je důležité, teprve přijde.
Zobrazení článku
Do projektu si přidejte další stránku, pojmenujte ji Article.aspx a zvolte Select Master Page, abyste mohli vybrat, kam chcete tuto stránku začlenit. Opět si otevřete okno kódu a místo vygenerovaného kódu tam vložte tento:
<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" Title="Článek v blogu" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:SimpleBlogCS %>"
SelectCommand="SELECT [PublishedDate], [Title], [Html] FROM [Articles] WHERE ([ArticleId] = @ArticleId)">
<SelectParameters>
<asp:QueryStringParameter Name="ArticleId" QueryStringField="id" Type="Int32" />
</SelectParameters>
</asp:SqlDataSource>
<br />
<asp:FormView ID="FormView1" runat="server" DataSourceID="SqlDataSource1">
<ItemTemplate>
<h1><asp:Literal ID="Literal1" runat="server" Text='<%# Eval("Title") %>'></asp:Literal> </h1>
<p>Publikováno: <strong><asp:Literal ID="Literal2" runat="server" Text='<%# Eval("PublishedDate", "{0:D}") %>'></asp:Literal></strong></p>
<p>Kategorie:
<asp:SqlDataSource ID="SqlDataSource2" runat="server" ConnectionString="<%$ ConnectionStrings:SimpleBlogCS %>"
SelectCommand="SELECT [Title] FROM [Categories] WHERE [CategoryId] IN (SELECT [CategoryId] FROM [ArticleCategories] WHERE [ArticleId] = @ArticleId)">
<SelectParameters>
<asp:QueryStringParameter Name="ArticleId" QueryStringField="id" Type="Int32" />
</SelectParameters>
</asp:SqlDataSource>
<asp:Repeater ID="Repeater1" runat="server" DataSourceID="SqlDataSource2">
<ItemTemplate><strong><%#Eval("Title") %></strong></ItemTemplate>
<SeparatorTemplate>, </SeparatorTemplate>
</asp:Repeater>
</p>
<hr />
<p><asp:Literal ID="Literal3" runat="server" Text='<%# Eval("Html") %>'></asp:Literal> </p>
<p style="text-align: center"><asp:HyperLink ID="HyperLink1" runat="server" NavigateUrl="~/Default.aspx">[Home]</asp:HyperLink></p>
</ItemTemplate>
</asp:FormView>
</asp:Content>
Přidali jsme na stránku komponentu FormView, která zobrazí název článku jako nadpis 1. úrovně a dále vypíše datum publikování a text článku. Nakonec je odkaz na úvodní stránku blogu. Uvnitř je ještě jeden datový zdroj a komponenta Repeater, která vypisuje názvy kategorií přidružené k článku.
Vytváříme publikační službu
Ještě než do projektu přidáme webovou službu pro publikování článků, musíme si uvědomit, že komunikace probíhá pomocí XML-RPC, které .NET sám o sobě neumí. Buď si můžeme napsat vlastní handler na zpracovávání takových požadavků, nebo můžeme využít Xml-Rpc.net, což je projekt, který umožní vytvořit XML-RPC volanou webovou službu. Toto řešení je elegantnější a jednodušší, takže se jej budeme držet.
Stáhněte si tedy XML-RPC knihovnu a přidejte si ji do adresáře Bin do vaší webové aplikace. Raději jsem ji zkompiloval a nahrál sem, bude to pro vás jednodušší.
Nyní si do projektu přidejte nový Generic Handler MetaWeblog.ashx, který bude spouštět XML-RPC volané procedury. Dá se použít i webová služba, ale musí se zasahovat do konfigurace aplikace. Handler je jednodušší a pro naše účely plně postačuje.
Postupně budeme implementovat veškeré procedury, které budeme potřebovat. Zatím do handleru vložte tyto deklarace:
<%@ WebHandler Language= "VB" Class="MetaWeblog" %>
<%@ Assembly Name="CookComputing.XmlRpcV2" %>
Imports CookComputing.XmlRpc
Imports System.Data.SqlClient
<XmlRpcService()> _
Public Class MetaWeblog
Inherits XmlRpcService
Public Structure BlogInfo
Public blogid As String
Public url As String
Public blogName As String
End Structure
<XmlRpcMissingMapping(MappingAction.Ignore)> _
Public Structure Enclosure
Public length As Integer
Public type As String
Public url As String
End Structure
<XmlRpcMissingMapping(MappingAction.Ignore)> _
Public Structure Source
Public name As String
Public url As String
End Structure
<XmlRpcMissingMapping(MappingAction.Ignore)> _
Public Structure Post
<XmlRpcMissingMapping(MappingAction.Error)> Public dateCreated As DateTime
<XmlRpcMissingMapping(MappingAction.Error)> Public description As String
<XmlRpcMissingMapping(MappingAction.Error)> Public title As String
Public categories As String()
Public link As String
Public permalink As String
Public postid As Object
End Structure
Public Structure CategoryInfo
Public description As String
Public title As String
Public categoryid As String
End Structure
Public Structure Category
Public categoryId As String
Public categoryName As String
End Structure
Public Structure FileData
Public bits As Byte()
Public name As String
Public type As String
End Structure
Public Structure UrlData
Public url As String
End Structure
Private Function ValidateUser(ByVal username As String, ByVal password As String) As Boolean
If (username = "tomas" And password = "tomas") Then
Return True
Else
Throw New XmlRpcException("Invalid username or password!")
End If
End Function
Private Function ValidateUserAgainstBlog(ByVal username As String, ByVal password As String, ByVal blogid As String) As Boolean
Return ValidateUser(username, password)
End Function
Private Function ValidateUserAgainstPost(ByVal username As String, ByVal password As String, ByVal postid As String) As Boolean
Return ValidateUser(username, password)
End Function
Private con As SqlConnection
Private com As SqlCommand
Private Sub DBConnect()
con = New SqlConnection(ConfigurationManager.ConnectionStrings("SimpleBlogCS").ConnectionString)
con.Open()
com = New SqlCommand()
com.Connection = con
End Sub
Private Sub DBClose()
con.Close()
End Sub
Private Function GetAbsoluteApplicationPath() As String
Dim url As String = HttpContext.Current.Request.Url.AbsoluteUri
Return url.Substring(0, url.LastIndexOf("/") + 1)
End Function
End Class
Nás bude nyní zajímat třída MetaWeblog. Nahoře máme deklarace struktur, které se v MetaWeblog API používají, abychom je měli silně typované. Kromě toho jsem doplnil procedury ValidateUser, ValidateUserAgainstBlog a ValidateUserAgainstPost, které kontrolují oprávnění uživatele. Nepoužívám zde kvůli jednoduchosti žádné ASP.NET přihlašování, jen zkontroluji jméno a heslo (standardně jméno tomas a heslo tomas, změňte si dle libosti). První funkce kontroluje, jestli uživatel existuje a jestli má správné heslo, druhá kontroluje, jestli má uživatel přístup do daného blogu a třetí kontroluje, jestli může uživatel manipulovat s daným příspěvkem. Druhá a třetí by zároveň měly volat tu první, která ověří správnost hesla. V každé proceduře se totiž volá jen jedna z těchto funkcí.
Dále jsem vytvořil proměnné con (SqlConnection) a com (SqlCommand). Důležitý je hlavně com, pomocí kterého se budeme dotazovat databáze a posílat do ní příkazy. Navíc jsem řidal dvě procedury DbConnect a DBClose, které by se měly zavolat na začátku a na konci procedury (na začátku se k databázi připojíme a na konci se od ní odpojíme). První z nich připraví spojení a inicializuje com, který pak můžeme používat.
Poslední funkci, kterou jsem připravil, je GetAbsoluteApplicationPath, která vrátí absolutní URL kořenového adresáře aplikace, kterou budeme na několika místech potřebovat.
Implementace jednotlivých procedur
V této části najdete poznámky a kód každé z procedur, kterou byste měli implementovat. Všechny procedury umístěte do třídy v našem handleru.
blogger.GetUsersBlogs
Tato procedura vrátí všechny blogy, ke kterým má uživatel přístup. Jelikož v naší primitivní aplikaci podporujeme pouze jeden blog, vypíšeme jej natvrdo. Parametr url struktury BlogInfo je absolutní URL blogu (např. www.mujblog.cz).
<XmlRpcMethod("blogger.getUsersBlogs")> _
Function getUsersBlogs(ByVal appKey As String, ByVal username As String, ByVal password As String) As BlogInfo()
ValidateUser(username, password)
Dim blogs(0) As BlogInfo
blogs(0).blogid = 1
blogs(0).blogName = "SimpleBlog"
blogs(0).url = GetAbsoluteApplicationPath()
Return blogs
End Function
blogger.deletePost
Tato procedura vymaže článek v blogu. Vzdálené klíče v naší databázi jsou nastaveny tak, aby se automaticky zrušily vazby v tabulce ArticleCategories, nemusíme se tedy o nic starat, zkrátka jen smažeme článek. Funkce vrací vždy hodnotu True.
<XmlRpcMethod("blogger.deletePost")> _
Function deletePost(ByVal appKey As String, ByVal postid As String, ByVal username As String, ByVal password As String, ByVal publish As Boolean) As Boolean
ValidateUserAgainstPost(username, password, postid)
DBConnect()
com.CommandText = "DELETE FROM [Articles] WHERE [ArticleId] = @ArticleId"
com.Parameters.AddWithValue("ArticleId", postid)
com.ExecuteNonQuery()
DBClose()
Return True
End Function
metaWeblog.newPost
Tato procedura přidá nový článek do blogu. Nejprve tedy vytvoříme nový záznam v tabulce Articles a rovnou zjistíme ArticleId nově vytvořeného článku, protože jej budeme potřebovat pro přiřazení kategorií. Je to také návratová hodnota této funkce. Navíc musíme provázat kategorie s článkem. Problém může nastat s parametrem dateCreated. Pokud datum v klientské aplikaci zvolíme ručně, pošle se a vše je v pořádku. Pokud ale datum v klientovi nevyplníme, parametr se nám nezinicializuje a datum ukazuje na 1.1.0001, což se nelíbí databázi. Pokud je tedy rok menší než 1900, je jasné, že se datum nezinicializovalo a nastavíme je na Now, tedy aktuální čas.
<XmlRpcMethod("metaWeblog.newPost")> _
Function newPost(ByVal blogid As String, ByVal username As String, ByVal password As String, ByVal post As Post, ByVal publish As Boolean) As String
ValidateUserAgainstBlog(username, password, blogid)
DBConnect()
com.CommandText = "INSERT INTO [Articles] ([Title], [Html], [Visible], [PublishedDate]) VALUES (@Title, @Html, @Visible, @PublishedDate); SELECT SCOPE_IDENTITY()"
If post.dateCreated.Year < 1900 Then post.dateCreated = Now
com.Parameters.AddWithValue("PublishedDate", post.dateCreated)
com.Parameters.AddWithValue("Title", post.title)
com.Parameters.AddWithValue("Html", post.description)
com.Parameters.AddWithValue("Visible", publish)
Dim postid As String = com.ExecuteScalar().ToString()
For Each cat As String In post.categories
com.Parameters.Clear()
com.CommandText = "DECLARE @CategoryId INT; SELECT @CategoryId = [CategoryId] FROM [Categories] WHERE [Title] = @Title; INSERT INTO [ArticleCategories] ([ArticleId], [CategoryId]) VALUES (@ArticleId, @CategoryId)"
com.Parameters.AddWithValue("ArticleId", postid)
com.Parameters.AddWithValue("Title", cat)
com.ExecuteNonQuery()
Next
DBClose()
Return postid
End Function
metaWeblog.editPost
Tato procedura upraví článek. Po editaci smažeme všechny vazby, které se k tomuto článku vztahují, projdeme pole categories a kategorie přidáme. Je to asi jednodušší než zjistit všechny přidružené kategorie a nové přidat a přebývající vymazat. Funkce vrací vždy hodnotu True. O datu platí to samé, co u přechozí procedury. Musíme dát pozor, aby nezůstalo nezinicializované, databáze to nevezme.
<XmlRpcMethod("metaWeblog.editPost")> _
Function editPost(ByVal postid As String, ByVal username As String, ByVal password As String, ByVal post As Post, ByVal publish As Boolean) As Boolean
ValidateUserAgainstPost(username, password, postid)
DBConnect()
com.CommandText = "UPDATE [Articles] SET [Title] = @Title, [Html] = @Html, [Visible] = @Visible, [PublishedDate] = @PublishedDate WHERE [ArticleId] = @ArticleId"
com.Parameters.AddWithValue("ArticleId", postid)
com.Parameters.AddWithValue("Title", post.title)
com.Parameters.AddWithValue("Html", post.description)
com.Parameters.AddWithValue("Visible", publish)
If post.dateCreated.Year < 1900 Then post.dateCreated = Now
com.Parameters.AddWithValue("PublishedDate", post.dateCreated)
com.ExecuteNonQuery()
com.Parameters.Clear()
com.CommandText = "DELETE FROM [ArticleCategories] WHERE [ArticleId] = @ArticleId"
com.Parameters.AddWithValue("ArticleId", postid)
com.ExecuteNonQuery()
For Each cat As String In post.categories
com.Parameters.Clear()
com.CommandText = "DECLARE @CategoryId INT; SELECT @CategoryId = [CategoryId] FROM [Categories] WHERE [Title] = @Title; INSERT INTO [ArticleCategories] ([ArticleId], [CategoryId]) VALUES (@ArticleId, @CategoryId)"
com.Parameters.AddWithValue("ArticleId", postid)
com.Parameters.AddWithValue("Title", cat)
com.ExecuteNonQuery()
Next
DBClose()
Return True
End Function
metaWeblog.getCategories
Tato procedura vrátí všechny kategorie článků na blogu. Nejjednodušší je vytvořit si objekt List(Of CategoryInfo), což je silně typovaný seznam, vybrat kategorie z databáze a metodou Add je do seznamu přidat. Procedura musí vrátit pole, které ze seznamu dostaneme zavoláním metody ToArray. Doporučuji nastavit kategoriím title i description stejné, Windows Live Writer totiž v seznamu kategorií zobrazuje title, kdežto Word 2007 zobrazuje description.
<XmlRpcMethod("metaWeblog.getCategories")> _
Function getCategories(ByVal blogid As String, ByVal username As String, ByVal password As String) As CategoryInfo()
ValidateUserAgainstBlog(username, password, blogid)
DBConnect()
com.CommandText = "SELECT [CategoryId], [Title] FROM [Categories]"
Dim r As SqlDataReader = com.ExecuteReader()
Dim cats As New Collections.Generic.List(Of CategoryInfo)
While r.Read
Dim cat As New CategoryInfo
cat.categoryid = r("CategoryId")
cat.title = r("Title")
cat.description = r("Title")
cats.Add(cat)
End While
r.Close()
DBClose()
Return cats.ToArray()
End Function
metaWeblog.getPost
Tato procedura vrátí kompletní informace o daném článku. Vybereme je tedy ze seznamu a podobným způsobem jako v předchozí proceduře naplníme i pole kategorií. Jediný problém je v tom, že Windows Live Writer název článku při odesílání provede HTMLEncode, což nahrazuje speciální znaky pomocí ekvivalentních HTML přepisů, ale zřejmě to provede dvakrát, takže musíme zavolat HTMLDecode, aby se přepisy převedly zpět, jinak jsou ve Wordu 2007 zobrazeny chybně.
<XmlRpcMethod("metaWeblog.getPost")> _
Function getPost(ByVal postid As String, ByVal username As String, ByVal password As String) As Post
ValidateUserAgainstPost(username, password, postid)
DBConnect()
com.CommandText = "SELECT [Title], [Html], [PublishedDate], [Visible] FROM [Articles] WHERE [ArticleId] = @ArticleId"
com.Parameters.AddWithValue("ArticleId", postid)
Dim p As New Post
Dim r As SqlDataReader = com.ExecuteReader()
r.Read()
p.title = HttpUtility.HtmlDecode(r("Title"))
p.description = r("Html")
p.postid = postid
p.dateCreated = r("PublishedDate")
p.permalink = GetAbsoluteApplicationPath() & "Article.aspx?id=" & postid
p.link = p.permalink
r.Close()
com.Parameters.Clear()
com.CommandText = "SELECT [Title] FROM [Categories] WHERE [CategoryId] IN (SELECT [CategoryId] FROM [ArticleCategories] WHERE [ArticleId] = @ArticleId)"
com.Parameters.AddWithValue("ArticleId", postid)
Dim cats As New Collections.Generic.List(Of String)
Try
r = com.ExecuteReader()
Catch
MsgBox(Err.Description)
End Try
While r.Read()
cats.Add(r("Title"))
End While
p.categories = cats.ToArray()
r.Close()
DBClose()
Return p
End Function
metaWeblog.getRecentPosts
Tato procedura vrací několik posledních příspěvků v blogu (počet si klient zvolí). Zde je to velmi podobné předchozí proceduře, samozřejmě je i problém se zakódováním speciálních znaků, nemusíme zde však složitě vypisovat kategorie, aplikace je podle této procedury nezjišťuje, zavolá si getPost.
<XmlRpcMethod("metaWeblog.getRecentPosts")> _
Function getRecentPosts(ByVal blogid As String, ByVal username As String, ByVal password As String, ByVal numberOfPosts As Integer) As Post()
ValidateUserAgainstBlog(username, password, blogid)
DBConnect()
com.CommandText = "SELECT TOP (@Count) [ArticleId], [Title], [Html], [PublishedDate], [Visible] FROM [Articles] ORDER BY [PublishedDate] DESC"
com.Parameters.AddWithValue("Count", numberOfPosts)
Dim posts As New Collections.Generic.List(Of Post)
Dim r As SqlDataReader = com.ExecuteReader()
While r.Read()
Dim p As New Post
p.title = HttpUtility.HtmlDecode(r("Title"))
p.description = r("Html")
p.postid = r("ArticleId")
p.dateCreated = r("PublishedDate")
p.permalink = GetAbsoluteApplicationPath() & "Article.aspx?id=" & r("ArticleId")
p.link = p.permalink
posts.Add(p)
End While
r.Close()
DBClose()
Return posts.ToArray()
End Function
metaWeblog.newMediaObject
Poslední a nejproblematičtější procedurou je tato. Pomocí ní se totiž nahrávají na blog soubory. Podle mě je ale navržená poněkud nešťastně, protože soubory se váží na celý blog a nikoliv na článek. Server v skutečnosti neví, ke kterému článku soubor patří, posílá se totiž jen ID blogu a název souboru. Pokud soubor s daným názvem již existuje, má být podle specifikace přepsán, což je ale další problém. Představte si situaci, kdy máte dva články a v každém je úplně jiný obrázek, který se však jmenuje stejně. První článek odešlete a v okamžiku, kdy odešlete ten druhý, soubor má stejný název a tím pádem se přepíše. Server totiž neví, jestli upravuje obrázek již existujícího článku, nebo jestli je tento obrázek k článku novému. A nemá ani šanci to zjistit, protože obrázky se posílají před přidáním nebo editací článku, ten ani nemusí mít ještě přiřazené id.
Word 2007 tento problém řeší elegantně - do názvu souboru přibalí aktuální datum a čas. To ale také není ideální, protože pokud článek upravíte a pošlete znovu, jméno souboru se změní a starý obrázek na serveru zůstane. Tohle děláte měsíc a máte server plný zbytečných obrázků. Jediná možnost je mít databázi vazeb souborů a článků a při přidání nebo editaci článku parsovat HTML a najít soubory, na které odkazuje, ty si uložit do databáze a při editaci nebo smazání článku přidružené soubory vymazat. Ne, že by šlo o neřešitelný problém, parsování je poměrně jednoduché, pokud použijete třeba HTMLAgilityPack, ovšem je to obcházení špatně navrženého standardu. Windows Live Writer si většinu příspěvků uchovává lokálně, takže zná jména souborů, ale pokud blog spravujeme z jiných počítačů, soubor se přepsat může.
My se s tímto zatím trápit nebudeme, možná v některém z příštích článků, ale máme zde ještě jeden problém. První parametr této procedury je blogid, který má být typu String. Word 2007 má v implementaci této procedury chybu a parametr blogid posílá jakotyp Object a protože většina databází má primární klíče typu Integer, pošle se parametr jako číslo a naše XMLRpc knihovna se s tím nevyrovná. Jediným řešením je implementovat tuto proceduru dvakrát, jednou pro String a jednou pro Object, přičemž ta druhá převede blogid na String a zavolá tu první.
Do projektu si musíte ještě přidat složku Files, do které budeme ukládat přiložené soubory.
Tato procedura vrací absolutní URL k souboru, aby si aplikace mohla obrázek stáhnout.
<XmlRpcMethod("metaWeblog.newMediaObject")> _
Function newMediaObject(ByVal blogid As String, ByVal username As String, ByVal password As String, ByVal file As FileData) As UrlData
ValidateUserAgainstBlog(username, password, blogid)
file.name = io.Path.GetFileName(file.name)
Dim sw As New IO.FileStream(IO.Path.Combine(HttpContext.Current.Request.PhysicalApplicationPath, "Files\" & file.name), IO.FileMode.Create)
sw.Write(file.bits, 0, file.bits.Length)
sw.Close()
Dim u As New UrlData
u.url = GetAbsoluteApplicationPath() & "files/" & file.name
Return u
End Function
<XmlRpcMethod("metaWeblog.newMediaObject")> _
Function newMediaObject(ByVal blogid As Object, ByVal username As String, ByVal password As String, ByVal file As FileData) As UrlData
Return newMediaObject(blogid.ToString(), username, password, file)
End Function
Dokončení
To by mělo být celé. Můžete si zkusit nainstalovat aplikaci Windows Live Writer nebo vyzkoušet publikování ve Wordu 2007, jako typ blogovací služby musíte vybrat MetaWeblog API a adresa služby je adresa na náš handler (Live Writer se nejprve ptá na adresu blogu a pak až na adresu služby). Publikační službu si samozřejmě upravte tak, aby vám vyhovovala a aby pracovala s vaší databází. Protože tyto záležitosti se špatně ladí, doporučuji použít neocenitelný nástroj Fiddler pro sledování komunikace.
Jinak poslední moje upozornění je, že Word 2007 posílá krásné čisté XHTML (ne ty hrůzy jako předchozí verze), zatímco Live Writer se neobtěžuje a posílá jen HTML (neuzavírá odstavce), takže s XHTML validním webem se můžete rozloučit, pokud nechcete řešit konverzi na XHTML, která ovšem nefunguje vždy tak, jak by měla. Nechápu, proč negeneruje XHTML rovnou, ani starým prohlížečům to nevadí (a přiznejme si, kdo dnes používá Netscape) a mobilní zařízení se s tím poperou také velmi dobře.