I’ve continued enhancing the Stanza catalog I created earlier. New features include purchase links and Google Analytics tracking. I reverse-engineered the FeedBooks API and the AllRomanceBooks.com feeds to figure out how to do some of these things. Also I’ve been reading the OPDS spec (an industry-standard successor to the Stanza format) to add support for e-readers on other platforms. For example Aldiko officially supports OPDS, but it recognizes Stanza tags as well. It crashes if you try to open a PDF file however – so it just needs to be sent the right mime type.
Here’s the snippet where I reference the custom Analytics class I added:
p.StoreDescription += Mises.Domain.Mobile.Analytics.GetAnalyticsImageTag(this.request.RequestContext.HttpContext);
var content = new TextSyndicationContent(p.StoreDescription,
TextSyndicationContentKind.Html);
item.Content = content;And here’s how to add external links to a book info view:
item.Links.Add(
new SyndicationLink(
new Uri(string.Format("http://mises.org/store/Product.aspx?ProductId={0}&utm_source=MisesCatalog", p.ProductId)), "alternate", "Purchase at the Mises Store",
"text/html", 0));
MSDN has a page on how to get the MD5 hash of a string.
Easy enough, but if you follow their example exactly for a file and use File.ReadAllText() to get the string, you will get the wrong MD5 string for binary files. Instead, use File.ReadAllBytes() to bypass encoding issues. (This also applies to SHA1 hashing.)
private static string GetMD5Hash(string filePath)
{
byte[] computedHash = new MD5CryptoServiceProvider().ComputeHash(File.ReadAllBytes(filePath));
var sBuilder = new StringBuilder();
foreach (byte b in computedHash)
{
sBuilder.Append(b.ToString("x2").ToLower());
}
return sBuilder.ToString();
}
Stanza is a book reader for the iPhone/iPad. One of Stanza’s features is the ability to browse specially formatted book catalogs. While it has a number of built-in catalogs, you can also add your own. I have created such a catalog with ASP.Net MVC 2.0 (screenshots). The Stanza catalog format is pretty simple – just AtomPub with some proprietary attributes for images and things like search. This was a quick and easy project because the .Net Framework 4.0 has the System.ServiceModel.Syndication namespace which does all the RSS/Atom feed generation. We just have to add some custom attributes and serialize the feed to the browser.
Here is a quick overview of the code (Links are to the latest version of the source code in my SVN browser. You can get the project from SVN here (guest/guest).) The LiteratureCatalog and LiteratureCatalog.Tests projects have the relevant code.
Update: The Stanza catalog format works equally well with Aldiko, an e-reader for Android.
CatalogController.cs:
This is the default controller specified in global.asax. It defers to MisesFeeds to generate the feed items and to FeedResult to serialize and write out the feed.
Sample Method:
public FeedResult Journal(int journalId)
{
var feeds = new MisesFeeds(Request);
SyndicationFeed feed = feeds.GetJournalFeed(journalId);
return new FeedResult(new Atom10FeedFormatter(feed));
}MisesFeeds.cs
MisesFeed contains all the code to generate a SyndicationFeed object containing a List of SyndicationItem. Note the Stanza-specific links added in search list-builder and the final helper method:
item.Links.Add(new SyndicationLink(new Uri(DataFormat.GetAbsoluteURL(p.Logo)),
"x-stanza-cover-image-thumbnail", "", "image/jpeg", 0));
public SyndicationFeed CreateFeedFromSyndicationItemList(IEnumerable postItems, string title,
string description)
{
var feed = new SyndicationFeed(title, description, new Uri(feedUri), postItems)
{
Copyright = new TextSyndicationContent(Configuration.Copyright),
Language = "en-US"
};
var self = new SyndicationLink(new Uri(Host + HttpUtility.UrlEncode("/Catalog/")), "self", "", Type, 0);
feed.Links.Add(self);
feed.Links.Add(new SyndicationLink(new Uri(Host + "/Catalog/Search/?q={searchTerms}",true),"search","Search Catalog",Type,0));
return feed;
}FeedResult.cs:
FeedWriter inherits from ActionResult. It just writes the SyndicationFeed out with an XmlTextWriter:
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = !string.IsNullOrEmpty(ContentType) ? ContentType : "application/atom+xml";
if (ContentEncoding != null)
response.ContentEncoding = ContentEncoding;
if (feed != null)
using (var xmlwriter = new XmlTextWriter(response.Output))
{
xmlwriter.Formatting = Formatting.Indented;
feed.WriteTo(xmlwriter);
}
}Thanks to DamienG for the FeedResult class.
To see the catalog, get the Stanza app, tap “Get Books”, “Shared”, “Add Book Source”, then add the URL mises.org/catalog.
Adding a “Like” button using the Facebook JavaScript API is easy:
<div id="fb-root">
<fb:like href="<%=Request.Url.Host + Request.RawUrl %>"></fb:like>
</div>
<script language="javascript">
window.fbAsyncInit = function () {
FB.init({ appId: 'your app id', status: true, cookie: true,
xfbml: true
});
};
(function () {
var e = document.createElement('script'); e.async = true;
e.src = document.location.protocol +
'//connect.facebook.net/en_US/all.js';
document.getElementById('fb-root').appendChild(e);
} ());
</script>
Here’s a quick and dirty implementation of continuous paging with Windows Phone 7. These code snippets are from a project using the GalaSoft MVVM Light Toolkit and Ninject, however you can adapt to fit your model. The paging and data binding functionality is abstracted in the base class PagedListViewModelBase so that the paging functionality can be easily applied to different kinds of lists – the lists inherit from PagedObject, which has a NumberOfResults property.
XAML:
<ListBox x:Name="profiles" ItemsSource="{Binding PagedObjectList}" MouseLeftButtonDown="profiles_MouseLeftButtonDown" ItemTemplate="{StaticResource SearchItemDataTemplate}" >
</ListBox>XAML.cs:
private void profiles_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
((SomeViewModel)(this.DataContext)).RowTouchedCommandHandled(e); // this just calls the method in the viewmodel - you can call it directly if your aren't using MVVM
}SomeViewModel.cs: inherits from PagedListViewModelBase
/// This is the only view-specific code you need for rendering and paging. All the paging and databinding is handled by PagedListViewModelBase
/// SelectionChanged handlers can go here or in the base class
public override void GetDataFromWebService(object obj)
{
// Get Data:
if (!IsLoading)
{
IsLoading = true;
var svc = new ProfileWebService(_userService.Current(), webService);
svc.GetSearchResults(CurrentPage, OnItemsRetrieved);
}
}PagedListViewModelBase.cs: contains the databinding and paging functionality
protected ObservableCollection<PagedObject> _pagedObjectList;
// An ObservableCollection object generates NotifyPropertyChanged events to let the UI know when to update
public ObservableCollection<PagedObject> PagedObjectList
{
get
{
if (_pagedObjectList == null && !IsLoading)
{
if (!IsInDesignMode)
{
GetDataFromWebService(null);
}
else
{
// design mode
GenDesignTimeMockProfiles();
RaisePropertyChanged("PagedObjectList");
}
}
return _pagedObjectList;
}
set
{
_pagedObjectList = value;
}
}
protected int CurrentPage { get; set; }
protected bool IsLoading;
/// Handles touches from user and decided when to add the next page to the list
public void RowTouchedCommandHandled(MouseButtonEventArgs obj)
{
var personalProfile = ((PagedObject)(((FrameworkElement)obj.OriginalSource).DataContext));
if (personalProfile.GetType() != typeof(PersonalProfile))
return;
int index = PagedObjectList.IndexOf(personalProfile);
Debug.WriteLine("Current index: " + index);
Debug.WriteLine("NumberOfResults: " + personalProfile.NumberOfResults);
if (!IsLoading && personalProfile.NumberOfResults > PagedObjectList.Count && index > PagedObjectList.Count - 6) // if less than six items from the end, get the next page
{
CurrentPage += 1;
Debug.WriteLine("Next page: " + CurrentPage);
GetDataFromWebService(0);
}
}
public abstract void GetDataFromWebService(object obj);
protected void OnItemsRetrieved(List<PagedObject> pagedObjects, Error error)
{
if (error == null)
{
if (_pagedObjectList == null)
{
_pagedObjectList = new ObservableCollection<PagedObject>();
}
pagedObjects.ForEach(p => _pagedObjectList.Add(p));
RaisePropertyChanged("PagedObjectList");
IsLoading = false;
}
else
{
Error = error;
}
}
}
/// You don't need all these conversions if you use a single type for your lists.
/// .Net 4.0 adds support for co-variance, which allows casting a generic collection to base type
protected void OnItemsRetrieved(List<PersonalProfile> profileList, Error error)
{
var objList = new List<PagedObject>();
profileList.ForEach(objList.Add);
OnItemsRetrieved(objList, error);
}PagedObject.cs:
[DataContract]
public class PagedObject : ModelBase
{
/// <summary>
/// Total number of results for this search
/// </summary>
[DataMember]
public int NumberOfResults { get; set; }
}
Suppose that you have a DTO (data transfer object) that you want to convert into a parameter array to be saved to file or sent over the web. You could serialize it and convert it to XML or JSON. But maybe you want to send it in an HTTP POST or GET and you don’t want to know anything about the class itself. You could use Reflection to iterate through the properties and extract the property names and values and output them to a string.
For example, this code will convert a class into a string suitable for a REST API call:
var properties = prefs.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
properties.ToList().ForEach(property =>
{
var hasDataMemberAttribute = property.GetCustomAttributes(typeof(DataMemberAttribute), false);
if (hasDataMemberAttribute.Length == 1)
{
string name = property.Name.ToLower();
string value = String.Empty;
object objValue = property.GetValue(prefs, null);
if (null != objValue)
value = objValue.ToString();
// Only serialize properties marked with the [DataMember] attribute:
var hasBrokerMapAttribute = property.GetCustomAttributes(typeof(DataMemberBrokerMapAttribute),
false);
if (hasBrokerMapAttribute.Length == 1)
{
name = ((DataMemberBrokerMapAttribute)hasBrokerMapAttribute[0]).Key;
}
if (value.Length > 0)
{
filter.Append(String.Concat("/", name, "=", value));
}
}
});
Debug.WriteLine("Search Filter:" + filter);
return filter.ToString();
This code is designed to run inside a Yahoo Open App, but it can be adapted for any OpenSocial widget written with the JavaScript API. It uses OpenSocial to get the users unstructured location and YQL to convert it into GPS coordinates.
Read more…
The following JavaScript is used here to rotate the featured product every 30 seconds. I will leave the implementation of the JSON-emitting REST api for another post.
<script src="/Javascript/jquery-latest.min.js" type="text/javascript"><!--mce:0--></script>
<script src="/Javascript/jquery-jtemplates.js" type="text/javascript"><!--mce:1--></script>
<script type="text/javascript"><!--mce:2--></script>
<div id="FeaturedProduct" class="outerbox"></div>
<textarea id="productTemplate" style="display: none;">
<div class="outerbox">
<div class="box featured">
<h2 >
Featured</h2>
<div class="item">
<div class="bord-se">
<div class="bord-ne">
<div class="bord-s">
<a href="/store/Product.aspx?ProductId={$T.ProductId}&utm_source=Homepage&utm_medium=FeaturedProd&utm_term=Widget&utm_campaign=Featured_Widget" mce_href="/store/Product.aspx?ProductId={$T.ProductId}&utm_source=Homepage&utm_medium=FeaturedProd&utm_term=Widget&utm_campaign=Featured_Widget" id="A1">
<img class="FeaturedProduct" src="{$T.ThumbnailUrl}" mce_src="{$T.ThumbnailUrl}" alt="featured" style="border-width:0px;" mce_style="border-width: 0px;" />
</a>
</div>
</div>
</div>
</div>
<h4>
<a href="/store/Product.aspx?ProductId={$T.ProductId}&utm_source=Homepage&utm_medium=FeaturedProd&utm_term=Widget&utm_campaign=Featured_Widget" mce_href="/store/Product.aspx?ProductId={$T.ProductId}&utm_source=Homepage&utm_medium=FeaturedProd&utm_term=Widget&utm_campaign=Featured_Widget">{$T.Name}</a></h4>
<span>Author: </span>
<a href="/store/search.aspx?m={$T.AuthorId}" mce_href="/store/search.aspx?m={$T.AuthorId}">{$T.Author}</a>
<span class="Price">
${$T.Price}</span>
{$T.Summary}
<a class="more" href="/store/New-Products-C52.aspx" mce_href="/store/New-Products-C52.aspx">view all…</a>
</div>
</div>
</textarea>
I did a presentation last week on AES encryption techniques in .Net.
I’ll post some details here later, but for now, I’ve uploaded a zip file with the project code.
Here’s the key bit:
string key = "1234567891123456";
string secret = @"Sometimes I sing Shania Twain's ""I Feel Like A Woman!"" in the shower.";
Console.WriteLine("basic:");
EncryptString(key, secret);
Console.ReadKey();
Console.WriteLine("salt the secret:");
// good when there are multiple machines but a dynamic global shared secret (for example, Profile Create Date or User ID)
string secret2 = secret + " ###" + DateTime.Now.Millisecond;
EncryptString(key, secret2);
secret2 = secret + " ###" + DateTime.Now.Millisecond;
EncryptString(key, secret2);
Console.ReadKey();
Console.WriteLine("salt the key:");
// good when the same machine encrypts/decrepts
string uniqueMachineIdentifier = MachineId.GetProcessorID();
Console.WriteLine("MachineId: " + uniqueMachineIdentifier);
EncryptString(key + uniqueMachineIdentifier, secret);
Console.ReadKey();
Console.WriteLine("SHA1 hash the passphrase with a salt:");
// note: talk about why hashing is good
SHA1 sha = new SHA1CryptoServiceProvider();
// This is one implementation of the abstract class SHA1.
string password = "this is my user password and/or userid";
byte[] saltedKey = Encoding.Default.GetBytes(key + password);
byte[] result = sha.ComputeHash(saltedKey);
EncryptString(Convert.ToBase64String(result), secret);
Console.ReadKey();
Recent Comments