预测:多云
多平台 Windows Azure 存储
Joseph Fultz
Windows Azure 存储绝非是设备特定技术,这是件好事。本月我将看一看以下三个移动平台上的开发:Windows Phone 7、jQuery 和 Android。
为此,我将为每个平台创建一个简单的应用程序,该程序将进行 REST 调用,以从预定的 Windows Azure 存储容器获得图像列表,并以电影胶片的形式显示缩略图,同时使显示所选图像的屏幕保持平衡,如图1 所示。
图 1 并排显示的存储图像查看器
准备存储容器
我需要一个 Windows Azure 存储帐户,以及所用上载工具的主访问密钥。如果要从正在开发的客户端进行安全访问,我同样需要该密钥。这些信息可在 Windows Azure 平台管理门户中找到。
我从 Internet 上随意找了几张图像,并用自己的计算机上载。我没有写上载代码,而是使用了 Windows Azure 存储浏览器,该浏览器可在 azurestorageexplorer.codeplex.com 上找到。出于后面说明的原因,图像应小于 1 MB。如果正确使用此代码,则图像最好为 512 KB 或更小。我将创建一个名为 Filecabinet 的容器,创建容器并上载图像后,Windows Azure 便准备好了。
平台模式
每个平台都有其特定的构造、启用程序和约束。对于 Silverlight 和 Android 客户端,我采用了封送处理数据的熟悉模型和供 UI 使用的对象集合。由于 jQuery 支持模板,在这种情况下,我发现直接通过 jQuery 提取 XML 和生成所需的 HTML 要容易得多,这使得 jQuery 实现非常直接。我将不详细介绍它的流程,但是对于另外两个示例,我将会多介绍一些背景知识。
Windows Phone 7 和 Silverlight
如果您熟悉 Silverlight 的开发,那么对在 Visual Studio 2010 中创建新的 Windows Phone 7 应用程序项目将不会有问题。如不熟悉,则对于该示例,您需要了解可观察的集合 (bit.ly/18sPUF)、常用 XAML 控件(如 StackPanel 和 ListBox)以及 WebClient。
开始时,应用程序的主页会对 Windows Azure 存储进行 REST 调用,调用有意设计为异步的。Windows Phone 7 强制使用此模式来确保任何应用程序最终不会阻塞和锁定设备。从调用检索到的数据将置于数据上下文中,类型为 ObservableCollection<>。
这样,容器就可以获得通知,并在集合中的数据更新时获得更改,而无需我明确执行刷新。使用 Silverlight 创建复杂的 UI 会很复杂,但对于相对简单的任务(例如此任务)则提供了较低的入门门槛。由于在 UI 中的触摸和物理支持中添加了内置的绑定和服务调用支持,电影胶片到缩放视图的变化如同使用数据绑定网格编写 ASP.NET 页面一样简单。
Android 活动
Android 推出了自己的平台模式。幸运的是,这个模式并非不可思议或难以理解。对于主要从事 .NET 开发的人员而言,映射到熟悉的结构、术语和技术是件轻而易举的事。在 Android 中,您想与用户进行的几乎任何交互都以活动对象表示。可将活动视为以下可组合生成应用程序的四大基本元素之一:活动、服务、广播接收器和内容提供程序。
为简单起见,我保留了活动中的所有代码。在更逼真的实现中,用以提取和操作 Windows Azure 存储中的图像的代码需要在服务中实现。
此示例更像是将数据库访问置于窗体后面的代码中。从 Windows 的角度看,您可以将活动视为 Windows 窗体,其有方法 - 事实上,是有需要 来保存和还原状态。活动和 Windows 窗体具有很类似的生命周期;应用程序可以由一对多的活动构成,但是一次只能有一个活动与用户进行交互。有关 Android 应用程序的更多基础知识,请转至 bit.ly/d3c7C。
此示例应用程序将由一个活动 (bit.ly/GmWui)、一个 BaseAdapter (bit.ly/g0J2Qx) 和一个用以存放图像相关信息的自定义对象构成。
创建 UI
每个 UI 模式下的常见任务包括:进行 REST 调用、将返回值封送到有用的数据类型、绑定和显示,以及处理单击事件以显示图像的缩放视图。首先,我要检查从 REST Get 返回的数据,以及在每个示例中让数据变得更易于使用所需的内容。
数据
在应用程序代码内,我更喜欢处理对象且无需经常操作 XML,因此,我创建了一个与存储调用返回的 XML 相匹配的对象。这类似于图 2 中的代码。
图 2 与存储调用返回的 XML 相匹配的对象
<EnumerationResults ContainerName="http://jofultz.blob.core.windows.
net/filecabinet">
<Blobs>
<Blob>
<Name>2010-12-12+10.40.06.jpg</Name>
<Url>http://jofultz.blob.core.windows.
net/filecabinet/2010-12-12+10.40.06.jpg</Url>
<LastModified>Sun, 02 Jan 2011 02:24:24 GMT</LastModified>
<Etag>0x8CD783B4E56116C</Etag>
<Size>263506</Size>
<ContentType>image/jpeg</ContentType>
<ContentEncoding />
<ContentLanguage />
</Blob>
<Blob>
<Name>cookie-monster.jpg</Name>
<Url>http://jofultz.blob.core.windows.
net/filecabinet/cookie-monster.jpg</Url>
<LastModified>Mon, 27 Dec 2010 09:40:18 GMT</LastModified>
<Etag>0x8CD73C13542256C</Etag>
<Size>265995</Size>
<ContentType>image/jpeg</ContentType>
<ContentEncoding />
<ContentLanguage />
</Blob>
<Blob>
之后,我创建了一个有代表性的对象来存放我想要的信息。 通常,我会关注名称和 URL 字段。 图 3 提供对每个示例所用对象的相关声明的描述。
图 3 每个客户端示例中所用对象的对象声明
Silverlight | Android | jQuery |
public static ObservableCollection<StorageImage> ImageInfoList {get;set;} ... public class StorageImage { public string Name{get;set;} public string Url {get;set;} public BitmapImage Image {get;set;} } |
public ArrayList<BlobInfo> BlobList= null; ... public class BlobInfo { public String Name; public String Url; public Bitmap ImageBitmap;} |
None 直接使用 XML。 |
创建 UI
幸运的是,在 Android 和 Silverlight 中定义 UI 的方式与其使用标记的方式相似,因此,理解一个几乎就可以保证能够理解另一个。 此外,这两者均使用 XML 标记来定义 UI。
jQuery 实现只是使用 HTML 并动态生成一些元素。 写标记时,三者之间的最大区别为使用的控件/元素的知识。
从 Windows Phone 7 XAML 开始,打开 MainPage.xaml,然后添加 <Grid/>,其将包含电影胶片的 <ListBox />。 列表框包含用于控制电影胶片数据呈现方式的 <ItemTemplate/> 和 <DataTemplate />。 此外在网格中还有一个 <Image/> 控件,用于操纵图像的缩放视图。
对于 Android,我将使用包含以下两个小组件的 LinearLayout 建立一个类似的 UI 构造:Gallery 和 ImageView。 对于熟悉 Silverlight 或 Windows Presentation Foundation (WPF) 的人而言,LinearLayout 类似于 StackPanel。 小组件即为控件,这大致可以等同于包含以下两个元素的 StackPanel:图像的水平 ListBox 和图像控件。 请注意,这也几乎完全是 Silverlight 示例中所使用的。 Gallery 提供缩略图的电影胶片样式视图,ImageView 显示所选缩略图的缩放视图。
对于 jQuery 示例,我将有一个空的 <p/>,id 名称为“output”;在这里将通过 jQuery 动态添加缩略图视图。
因为要处理标记内的绑定和呈现,Silverlight 标记要长一些。 对于 Android,则需要略微多一些的代码来处理数据加载。 三者的标记如图 4 所示。
图 4 标记的区别
元素 | Silverlight | Android | jQuery |
电影胶片 | <ListBox /> | <Gallery /> | <p /> |
缩放视图 | <Image /> | <ImageView /> | <img /> |
开始标记:Silverlight
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ListBox Name="ImageList" ItemsSource="{Binding}" MaxWidth="400"
Background="#00C23838" Margin="28,16,28,577" BorderBrush="#32D4BA29"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
SelectionChanged="ImageList_SelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal">
</StackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<Image Source="{Binding Image}" Height="110" Width="150"
Name="{Binding Name}" ></Image>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Image Height="447" HorizontalAlignment="Left" Margin="28,123,0,0"
Name="ImageViewer" Stretch="Fill" VerticalAlignment="Top" Width="400" />
</Grid>
</Grid>
开始标记:Android
<LinearLayout android:id="@+id/LinearLayout01"android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical">
<Gallery xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/Thumbnails" android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<ImageView android:id="@+id/ZoomView"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</LinearLayout>
开始标记:jQuery
<p id="output">
</p>
<p>
<img id="zoomview" height="480" width="320" />
</p>
主代码
下一步是创建可联系 Windows Azure 存储以获取 blob 信息的代码。 提取请求采用 http://[your storage account].blob.core.windows. net/[your container]?comp=list 形式;我的请求是 http://jofultz.blob.core.windows. net/filecabinet?comp=list。
由于结果为 XML,因此,我在 Silverlight 中使用 LINQ 来提供很好的对象图和访问模式。 我本想将一些类似的东西用于 Java,但最终为跳过对不熟悉技术的调试,我选择了只使用 DocumentBuilder API 提供的文档对象模型 (DOM) 样式的界面。 由于这是针对几个元素的只读操作,因此并非难事。 对于 jQuery,这只是一个选择器样式的查询,查询路径与处理 DOM 类似。
图 5 中显示的提取方法非常简单:由于容器是公共的,因而我无须处理代理或任何特殊的身份验证。
图 5 提取方法:
Silverlight
WebClient StorageClient = new WebClient();
StorageClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(GetBlobsCompleted);
StorageClient.DownloadStringAsync(new Uri(AZURE_STORAGE_URL, UriKind.Absolute));
Android
public InputStream GetBlobs() {
HttpURLConnection uc;
InputStream returnStream=null;
try{
URL u = new URL(AZURE_STORAGE_URL);
uc = (HttpURLConnection) u.openConnection(); //u.openConnection(proxy);
uc.setRequestProperty("Accept", "*/*");
uc.setRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7");
uc.setRequestProperty("Accept-Language", "en-us,en;q=0.5");
uc.setRequestProperty("Keep-Alive", "300");
uc.setRequestProperty("ucection", "keep-alive");
returnStream = uc.getInputStream();
jQuery
$(document).ready(function () {
$.ajax({
type:"GET",
url:"http://jofultz.blob.core.windows. net/filecabinet?comp=list",
dataType:"xml",
success:RenderData
});
});
对于 Silverlight 和 jQuery,由于请求是异步的,而响应将传递给回调以进行处理,因此我提供了用于处理响应的回调。 对于 Android,请求是同步发出的。 在 jQuery 和 Silverlight 中,我还需要使用一个支持方法来处理数据并将数据变为可用格式,而在 jQuery 中,将直接呈现数据。
接下来,我需要在每个示例中处理响应。 我将讨论每个示例,使其尽可能清晰明了。 从 Windows Phone 7 示例开始,我将实现 GetBlobsCompleted 方法。 我使用 LINQ 语法将 XML 梳理为我的自定义对象的可枚举列表,如图 6 所示。
图 6 GetBlobsCompleted 方法
IEnumerable<StorageImage> ImageList = from Blob in xDoc.Descendants("Blob")
select new StorageImage()
{
Name = Blob.Descendants("Name").First().Value,
Url = Blob.Descendants("Url").First().Value,
Image = new BitmapImage(
new Uri(Blob.Descendants("Url").First().Value, UriKind.Absolute))
};
MainPage.ImageInfoList.Clear();
foreach (StorageImage CurImg in ImageList)
{
MainPage.ImageInfoList.Add(CurImg);
}
我需要将对象移动到 ObservableCollection<StorageImage>。 我首先清除了列表,然后遍历了 LINQ 结果并将对象添加至我的 ObservableCollection。 由于集合在 DataContext 中,而 ListBox 已绑定到 DataContext,因此,集合会自动接收更改并更新 ListBox 的内容。
让我们继续了解 Java/Android,由于没有像 LINQ 这样的东西可用,我只好多费点事,手动将数据梳理出 DOM。 MarshalToBlobInfos 方法将使用 XML 文档对象并对其进行解析,以构建 BlobInfo 的 ArrayList。
这同 .NET、JavaScript 等中的任何其他 DOM 样式的访问相似,如图 7 所示。
图 7 MarshalToBlobInfos 方法
public ArrayList<BlobInfo> MarshalToBlobInfos(Document doc)
{
ArrayList<BlobInfo> returnBlobs = new ArrayList<BlobInfo>();
try {
Element root = doc.getDocumentElement();
NodeList items = root.getElementsByTagName("Blob");
for (int idxBlobs=0;idxBlobs<items.getLength();idxBlobs++){
BlobInfo blob = new BlobInfo();
Node item = items.item(idxBlobs);
NodeList blobProperties = item.getChildNodes();
for (int idxProps=0;idxProps<blobProperties.getLength();idxProps++){
Node property = blobProperties.item(idxProps);
String name = property.getNodeName();
if (name.equalsIgnoreCase("Name"))
blob.Name = property.getFirstChild().getNodeValue();
} else if (name.equalsIgnoreCase("Url")){
blob.Url = property.getFirstChild().getNodeValue();
}
}
returnBlobs.add(blob);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return returnBlobs;
}
最后,我写了用于执行结果和呈现所需 HTML 的 jQuery。 这是目前为止位数最少的代码。 但是,就便捷程度而言,它可能是最愚钝的:
function RenderData(xml) {
$(xml).find("Blob").each(
function () {
$("#output").append("<img margin=50 height=70 width=60 src=" +
$(this).find("Url").text() +
" title=" + $(this).find("Url").text() + " id=" +
$(this).find("Name").text() + ") /> ");
});
$("img").each(
function () {
$(this).click(
function () {
$("#zoomview").attr("src", $(this).attr("src"));
});
});
}
它需要 XML 的每个“Blob”元素,我为每个元素都写入了 <img /> 标记,并为 id 指定值“output”。我使用了相似的选择器结构来提取当前所需“Blob”元素中每个元素的值,以正确呈现 <img /> 标记。 然后,我为每个生成的 <img /> 元素创建了一个选择器,并添加了单击处理程序。
到目前为止,我的 Silverlight 代码对 Windows Azure 存储进行了 REST 调用,解析了结果并显示了电影胶片。 最后一步是为电影胶片添加单击控制程序以显示图像的缩放视图。 我为获取当前所选 StorageImage 并为图像控件源属性指定其图像属性值的 SelectionChanged 添加了处理程序:
private void ImageList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox lb = (ListBox)sender;
StorageImage img = (StorageImage) lb.SelectedItem;
ImageViewer.Source = img.Image;
}
这就是 Windows Phone 7 上的 Silverlight。 Android 则需要进行更多的工作。
由于有了 Windows Azure 存储的 REST 界面和直接的 XML 响应,因此无论是使用 .NET、JavaScript,还是本示例中针对 Android 的 Java,我都可以轻松进行调用和解析结果。 两个主要支持方法就绪后,我将向活动的 onCreate 方法添加代码。 在活动中,在恢复现有状态的行之后,我将添加代码行以调用 Windows Azure 存储并解析结果:
InputStream blobstream = GetBlobs();
DocumentBuilder docBuilder =
DocumentBuilderFactory.
newInstance().
newDocumentBuilder();
Document doc = docBuilder.parse(blobstream);
BlobList = MarshalToBlobInfos(doc);
如果代码在此刻运行,我将不会得到 UI,但是我想要 Windows Phone 7 示例中的电影胶片。 下一步是挂接 UI 元素。 我将调用 setContentView,传入源 id。 在本示例中,为 main.xml 的源 id。
如果需要更改活动的视图,我可以调用它并传入不同的源。 在上一个 main.xml 标记示例中,Gallery 和 ImageView 各自具有缩略图和 ZoomView 的 id。 我将使用这些 id 来引用小组件(控件),并为每个引用指定一个局部变量。 我将把下列代码直接放在之前代码的后面:
setContentView(R.layout.main);
mZoomView = (ImageView)findViewById(R.id.ZoomView);
mThumbnails = (Gallery) findViewById(R.id.Thumbnails);
现在,我需要创建适配器以帮助检索、格式化和排列集合中的每个项目。 需要先为缩略图库创建一个适配器,然后才可对该适配器进行设置。 我在项目中添加了一个新类 ImageAdapter,其扩展了 BaseAdapter。 当我将其设置为活动的适配器时,它应该调用 count,并在随后针对传入项目位置的每个项目调用 getView,我会将这用作 ArrayList 索引。 我会将指向活动的指针传递给 ImageAdapter,因为我需要上下文信息,特别是代表图像的 ArrayList。 ImageAdapter 的声明和构造函数如下所示:
public class ImageAdapter extends BaseAdapter
{
private Context mContext;
private ArrayList<BlobInfo> mBlobList=null;
public ImageAdapter(Context c)
{
mContext = c;
mBlobList = ((AzureImageViewer) mContext).BlobList;
}
public int getCount()
{
int count = mBlobList.size();
return count;
}
在 getView 方法中,有关适配器的实现如下:提取图像、创建缩略图并将原始图像保存到相关的 BlobInfo 对象。 对于实际的实现,人们只想预先提取一些图像并在用户滚动时清除这些图像,同时根据电影胶片的位置保持可用图像的移动窗口。 缩略图需要通过后台线程提取,同 Silverlight 和 jQuery 示例中要求的模式类似。
此外,项目的提取代码最好放于 getItem override 中。 了解了这些注意事项后,图 8 显示了 getView 的实现。
图 8 getView 实现
public View getView(int position, View convertView, ViewGroup parent)
{
ImageView imageView;
if (convertView == null)
{
imageView = new ImageView(mContext);
imageView.setLayoutParams(new Gallery.LayoutParams (85, 70));
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
imageView.setPadding(4, 4, 4, 4);
}
else
{
imageView = (ImageView) convertView;
}
BlobInfo CurrentBlob = mBlobList.get(position);
if (CurrentBlob.ImageBitmap == null)
{
Bitmap bm = getImageBitmap(CurrentBlob.Url);
imageView.setImageBitmap(bm);
CurrentBlob.ImageBitmap = bm;
}
else
{
imageView.setImageBitmap(CurrentBlob.ImageBitmap)
}
return imageView;
}
视图的初始状态由第一个条件语句完成,方法为配置状态或使用传递给函数的视图。 执行 if-else 后,视图就准备好了,但是我仍没有图像。 为解决该问题,我使用了位置参数来提取合适的 BlobInfo 对象,然后用 URL 属性来提取图像(在我的示例中,图像均为 .jpg),如图 9 所示。
图 9 提取图像
private Bitmap getImageBitmap(String url) {
Bitmap ImageBitmap = null;
try {
URL ImageURL = new URL(url);
URLConnection ImageConnection = ImageURL.openConnection();
ImageConnection.connect();
InputStream ImageStream = ImageConnection.getInputStream();
BufferedInputStream BufferedStream = new BufferedInputStream(ImageStream);
Log.e("Available buffer: ", String.valueOf(BufferedStream.available()));
ImageBitmap = BitmapFactory.decodeStream(BufferedStream);
BufferedStream.close();
ImageStream.close();
} catch (Exception e) {
Log.e("Error getting bitmap", e.getMessage());
}
return ImageBitmap;
}
为提取图像,我根据 URL 创建了新的连接,然后创建了 BufferedStream 以传递给 BitmapFactory.decodeStream 方法。 但是,decodeStream 和使用 BufferedStream 对象似乎有问题。 在此示例中,我将继续使用它,因为我知道所测试的大小为 1 MB 或更大的图像将不会正常工作;更糟的情况是其失败却没有提示。
避免这些问题的最好方法是编写代码来手动读取输入流,然后将其传递给 BitmapFactory 对象。 BitmapFactory 完成相应任务后,我将返回位图,设置 ImageBitmap 并将位图指定到相关的 BlobInfo 对象的 ImageBitmap 属性。 如果已经提取,我只需使用 BlobInfo 对象中的当前值即可。
运行时,我应该看到图像的电影胶片,但是,到目前为止,我还不能放大图像,选中缩略图也没有发生任何变化。 因此,我将为缩略图添加了一个单击侦听程序,并根据所选项目的位置引用 BlobInfo ArrayList 并使用生成的 BlobInfo ImageBitmap 属性作为 ZoomView(类型为 ImageView)setImageBitmap 调用的参数:
mThumbnails.setOnItemClickListener(new OnItemClickListener()
{
public void onItemClick (AdapterView<?> parent, View v, int position, long id)
{
BlobInfo CurrentBlob = BlobList.get(position);
mZoomView.setImageBitmap(CurrentBlob.ImageBitmap);
}
});
通用供应者
与快捷简单的 Windows Phone 7 和 jQuery 示例相比,Android 示例是最繁杂的实现。 这可能归因于我的熟悉程度,但是观察我需跨这三者编写的代码,我认为原因不仅仅局限于便捷程度。 事实是,无论使用哪种设备平台,使用 Windows Azure 均可为应用程序注入生命和能量。
如果没有他人帮忙的话,与 Windows Azure 存储的交互将没那么简单了。 在本示例中,访问可归结为通过 REST 提取列表,然后根据返回的内容发出后续 HTTP Get 以直接提取项目。
Joseph Fultz 是 AMD 的一名软件架构师,帮助定义门户、服务基础结构和实现的整体体系结构和策略。之前,他是 Microsoft 的软件架构师,协助 Microsoft 顶层企业和 ISV 客户定义体系结构和设计解决方案。