Damon Payne: Hand waving software architect

103db signal to noise ratio at < .03% total harmonic distortion
Solution Architect, software developer, geek
Damon Payne at Blogged
2009 Microsoft MVP - Client App Dev
2007 Microsoft MVP - Solution Architecture
 Tuesday, March 18, 2008

Securing ClickOnce Deployments

 

I have written in the past (http://www.damonpayne.com/PermaLink,guid,da078e56-eb5b-4616-8605-13c38d54efc3.aspx) about some of the plumbing involved in ClickOnce and Client Application Services geared towards preventing unauthorized operation of a client application.  At the time I noted that there is at least one issue with this level of “security”.  While it does keep unauthorized users from running your application, it doesn’t protect your code.  As has been lamented elsewhere, a ClickOnce application deployed on the Internet does not have any supported means of keeping people from getting your code.  Even with obfuscation this may be undesirable: we don’t want people getting a hold of the application manifest file and subsequently the assemblies.

A while back I started down a few paths to secure the code and the Application Deployment Manifest.  I have a pretty decent partial solution for protecting your code.  Read on.

Step 1: Security by Obscurity?

Your ClickOnce deployment consists of a deployment manifest, the executable, and all of the referenced assemblies and content files required to run your Windows Forms or WPF application.  By turning off directory browsing and not having any publicly available links to your .application file you can discourage casual spelunking.   I’m not very satisfied with this as the only aspect of a code protection solution. 

It’s fairly unlikely than an uneducated attacker (outside job) could guess the names of the assemblies that make up your application.  If only you could lock down the .application file…

Eliminating the Obvious Suspects

To get the obvious thoughts out of the way:

1)      You cannot protect a ClickOnce application deployment manifest using Forms authentication.  The client side aspects of ClickOnce that get installed with .NET 2.0 can’t handle Forms authentication.

2)      Windows Authentication is at best a poor option here, and not a real option for most people.  The performance of “Remember me” in an NTLM challenge form is flaky and is not supported in the Internet zone.

3)      ClickOnce does not support HTTP Basic authentication schemes.

An Idea

One nice thing about ClickOnce is that, like Silverlight, it’s really a Client Side technology.  You don’t NEED to be running Windows Server and IIS in order to benefit from these technologies.  However since I do know some Microsoft server side technologies I had an idea.

I wanted to associate a Product Key of some kind with a ClickOnce deployment.  Product Keys are not the final word on application licensing but it is a means of accountability.  Look at Windows Genuine Advantage: you can’t keep people from sharing CD keys but if you see the same CD key being used 1,000 times from different hosts all over the nation, you can shut it down.  Given a test deploy URL:

http://localhost:5785/CarSpot/MyApp.application

It would be nice to be able to do something like this:

http://localhost:5785/CarSpot/MyApp.application?ProductKey=ZZZZ-ZZZZ-ZZZZ-ZZZZ

… and have it be meaningful.

HttpHandler Attempt 1

To keep people from getting at my .application file, I thought a simple IHttpHandler implementation configured to intercept requests for the .application extension could be the answer. 

Web.config and sample code:

<add verb="GET,HEAD" path="*.application" type="ClickOnceHandler"/>

    public void ProcessRequest(HttpContext context)

    {

        string key = context.Request.QueryString["key"];

        if (ValidateKey(key))

        {

            //Go ahead and give up the .application file

        }

        else

        {

            throw new ApplicationException("Invalid or missing product Key");

        }

    }

I had a notion of allowing users to sign up for the application online by entering a privately exchanged product key in a web page:

The code behind for the install button is simple:

Response.Redirect("Publish/MyApp.application?key=" + _prodKeyTxt.Text);

Sadly, the solution is not this simple.  The Client side ClickOnce libraries exhibit some bizarre behavior.  Using Fiddler and the debugger, this is what I see:

1)      The .application file is requested and the HttpHandler validates the product key.

2)      Mysteriously, ClickOnce requests the entire URL of the .application file again: http://localhost:5785/CarSpot/Publish/MyApp.application?key=123; The HttpHandler validates the key and writes the contents of the .application file to the Response.

3)      Mysteriously (and infuriatingly) ClickOnce requests the .application file again: http://localhost:5785/CarSpot/Publish/MyApp.application ; note the missing piece of the query string.

I was unable to dig up any reason for what ClickOnce is doing here, but I do know that without getting that product key my scheme is ruined.  Looking at the .application file there’s a likely explanation:

<deploymentProvider codebase="http://127.0.0.1:5785/CarSpot /Publish/MyApp.application" />

This is where I left it for a long time, while dealing with some other things at work.

HttpHandler Attempt 2

The next, step, I felt, was to dynamically alter the application deployment file so that the provider codebase as recorded by the client included the product key on the URL.  I added some code to do this:

                XmlDocument doc = new XmlDocument();

                doc.Load(path);

                //Get around the issue of the query strong not being

                doc.DocumentElement["deployment"]["deploymentProvider"].Attributes["codebase"].Value += "?key=123";

However, this introduces another issue.  When you choose to give your ClickOnce application Full Permissions (Otherwise why not go the XBAP route?) you will find that you are signing it.   Look what Visual Studio did for me:

<publisherIdentity name="CN=CORNZOR\Damon" issuerKeyHash="" /><Signature Id="StrongNameSignature" {nothing to see here}</Signature></asmv1:assembly>

Yes, my laptop is named cornzor.  At any rate, this signature is there for your clients’ protection: if a man in the middle alters the XML payload of the .application file it will not verify and ClickOnce will refuse to do anything further.  Since I’m not a man in the middle I assumed there must be a way to dynamically do what I want.

http://msdn2.microsoft.com/en-us/library/acz3y3te.aspx

http://msdn2.microsoft.com/en-us/library/bb384246.aspx

I started out with the two links above, researching how the manifests are generated, and initially experimenting with manually generating un-signed deployment manifests from the command line.  After an infuriating XML character encoding setback, these articles did lead me to the answer.  Now I can protect my .application file in an appropriate fashion.

HttpHandler Complete

The articles above give us the tools we need to put the product key in the application manifest for each client and re-sign on the fly.

    /// <summary>

    /// Execute whatever business logic will make this a valid program key

    /// </summary>

    /// <param name="key"></param>

    /// <returns></returns>

    protected bool ValidateKey(string key)

    {

        return key.Equals("123");

    }

    /// <summary>

    /// Create a new signature in the deployment manifest

    /// </summary>

    /// <param name="path"></param>

    /// <param name="pfxPath"></param>

    protected void ReSign(string path, string pfxPath)

    {

        string mageArgFormat = "-Sign {0} -CertFile {1} -ToFile {2}";

        string mageExe = @"C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\mage.exe";

        string mageArgs = string.Format(mageArgFormat, path, pfxPath, path);

        ProcessStartInfo info = new ProcessStartInfo();

        info.Arguments = mageArgs;

        info.FileName = mageExe;

        info.UseShellExecute = false;

        info.RedirectStandardError = true;

        info.RedirectStandardOutput = true;

        Process p = Process.Start(info);

        p.WaitForExit();

        using (StreamReader stdOutReader = p.StandardOutput)

        {

            string stdOut = stdOutReader.ReadToEnd();

            //Error handling here

        }

 

        using (StreamReader stdErrReader = p.StandardError)

        {

            string stdErr = stdErrReader.ReadToEnd();

            //Error handling here

        }

    }

This method uses mage.exe to re-sign the application manifest after we’ve modified it using the BuildClientApplicationFile method.  There are two things to note about this method.  You will see that I’m referring to mage.exe from the Visual Studio directory here.  Obviously, installing Visual Studio on servers is not ideal.  Mage.exe also ships separately with the SDKs as indicated in the two articles I’ve linked, which is a more palatable install than Visual Studio.  I have not yet tested to see if mage.exe will properly operate on Windows Server 2003 by itself.

The second note on this method is that we are passing in a fully qualified path to a .pfx key file.  I’ve only tested this using the same .PFX key I used when I initially published the application.

    private static void BuildClientApplicationFile(string path, string signedPath, string key)

    {

        XmlDocument doc = new XmlDocument();

        doc.Load(path);

        //Get around the issue of the query string not being persisted by the client

        doc.DocumentElement["deployment"]["deploymentProvider"].Attributes["codebase"].Value += "?key=" +key;

        doc.Save(signedPath);

    }

This method stores a special version of the application deployment manifest for this particular client.  I didn’t keep this in memory due to ClickOnce’s odd habit of re-requesting the manifest many times.  This means I’ve got one copy of the manifest per client sitting on my server.  In my testing I’ve found two side-effects for publishing updates using this method:

1)      If I re-activate from the URL and updates have been published, the client does not get prompted

2)      If I run the program from the start menu, the client does get prompted for updates.

Here is the main HttpHandler method:

    public void ProcessRequest(HttpContext context)

    {

        string key = context.Request.QueryString["key"];

        if (ValidateKey(key))

        {

            //Just in case the client doesn't know what to do with .application

            context.Response.ContentType = "application/x-ms-application";

 

            //The current version deploy URL

            string path = context.Server.MapPath("~/Publish/CarSpot.MyApp.application");

 

            //Check for existing signed manifest for this user:

            //keeping a specially signed copy of the manifest per client

            string signedFileName = key + ".CarSpot.MyApp.application";

            string signedFileServerPath = "~/Publish/" + signedFileName;

            string signedPath = context.Server.MapPath(signedFileServerPath);

 

            if (!File.Exists(signedPath))

            {

                //Load the generated deployment manifest as an XML document to make it easier

                //to edit.

                BuildClientApplicationFile(path, signedPath,key);

 

                string pfx = context.Server.MapPath("~/Publish/MyGeneratedKey.pfx");

                ReSign(signedPath, pfx);

                //Redirect to original path now that we've created a signed one...

                string redirUrl = context.Request.Url.ToString();

                context.Response.Redirect(redirUrl);

            }

            else

            {

                context.Response.WriteFile(signedPath);

                context.Response.Flush();

            }

        }

        else

        {

            context.Server.Transfer("~/Error.aspx?errormessage=Missing or invalid Application key");

        }

    }

I feel confident this is safe, look at the appref-ms file generated from this install and put in the start menu on the client:

http://127.0.0.1:5785/CarSpot /Publish/MyApp.application?key=123#CarSpot.MyApp.application, Culture=neutral, PublicKeyToken=blahblah, processorArchitecture=msil

The key is part of the URL, so it should be passed to the server every time.

Further reading

Now I’ve locked down my deployment manifest, at least you have to have a valid product key to get access to it.  This is a large step towards completely locking down my application.  Since this is ASP.Net code that I control, I can log the remote hosts that use each product key and disable one if it appears to be too promiscuous. 

What about your other files?  If someone somehow figures out that there’s a file called CarSpot.Snark.Zing.dll in the ~/ClickOnce directory?  I have some ideas for this too, along the same lines as what we’ve done here.  You will note that one of the Publish options for ClickOnce is to “Use .deploy file extension”.  Sounds like an HttpModule could be created to handle the .deploy extension.

Microsoft’s behavior regarding ClickOnce baffles me, but here we have a means of securing your deployments.



Tuesday, March 18, 2008 10:05:56 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [3]  |  Trackback
 Sunday, March 16, 2008

I found this "atheist prayer" via Friendly Atheist.  I enjoy Friendly Atheist, but I wanted to point out an issue with this post.

"Our brains, which art in our heads, treasured be thy names. Thy reasoning come. The best you can do be done on earth as it is. Give us this day new insight to resolve conflicts and ease pain. And lead us not into supernatural explanations, deliver us from denial of logic. For thine is the kingdom of reason, and even though thy powers are limited, and you’re not always glorious, you are the best evolutionary adaptation we have for helping this earth now and forever and ever. So be it. "

I suppose this is meant as harmless humor, and I probably wouldn't flip out if it had been entitled the Atheist Credo or something similar.  Let's not kid ourselves though.  Equivocation is one of the main ways the un-religious are attacked today.  Sam Harris absolutely called this one right.  To call the extreme skepticism of the possibility of the existance of a god on the same plane as faith is one of the more popular parlor tricks of the faithful today.  To call acceptance of empirical evidence a "kind of faith" turns my FlipOut dial up to 11.  The faithful are having enough of a hayday with ridiculous equivocation arguments as it is.  Do we really need to add ammunition by creating secular versions of old catechisms?  I think not.

Main Entry:
1prayer Listen to the pronunciation of 1prayer
Pronunciation:
\ˈprer\
Function:
noun
Usage:
often attributive
Etymology:
Middle English, from Anglo-French priere, praiere, preiere, from Medieval Latin precaria, from Latin, feminine of precarius obtained by entreaty, from prec-, prex
Date:
14th century
1 a (1): an address (as a petition) to God or a god in word or thought <said a prayer for the success of the voyage>
 
Yeah, I think I'll abstain from using this term even in jest, lest it be misconstrued.


Sunday, March 16, 2008 6:06:29 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Saturday, March 15, 2008

Dan has posted an excellent article on implementing a Tree structure in C#.  He has beaten me to the punch, after talking about my "Tree of dependent tasks" idea over beers.  I should have something to add to the discussion in the coming weeks.



Saturday, March 15, 2008 3:45:31 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, March 14, 2008

http://www.ps3fanboy.com/2008/03/14/microsoft-predicts-blu-ray-irrelevance-in-12-18-months/

This is ludicrous talking head speak for the vast overwhelming majority of the movie-watching population.  If I can get better than 1.5mb/s DSL in the next 18 months I'll be shocked, let alone enough pipe to handle True HD video.  At this stage of the market, digital downloads and packaged optical media are NOT different versions of the same product, as these people seem to believe.  They are utterly different products.  38mb/s MPEG4 with 5 mb/s LPCM is not "the same" as the puny bitrates we get through video on demand at this time, and the average consumer can handle having a shelf full of optical discs much easier than they can prepare for terrabytes of digital storage medium.  How many average users have a backup strategy?  The first time you want to watch The Matrix but that hard drive died and you have to redownload you'll be mad.  When you realize the DRM might make re-downloading insanely painful or impossible (as Casey has shown) you'll be wishing you had a Blu-Ray player instead.

The market will figure this out, but not in 12 months, sorry Mr Pundit.

{Edit: I ironically realized after I posted that the words of someone linking to any website with "fanboy" in the name are probably deserving of a side of salt as well.  Oh well, it's the internet...}

Movies | Rant


Friday, March 14, 2008 3:37:05 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback

What an astouning letdown: I was on track for a red letter day yesterday.  My parts came in, it was sunny, and I drank some beer and BBQ-ed some poultry.  Hickory smoked BBQ chicken is among my favorite foods and I haven't been able to have it for months because my wife has a pregnancy aversion to most forms of chicken and it's also been ridiculously cold.  I went to best buy and got Bioshock only to discover later that one of my parts is either DOA, I've plugged something in wrong, or the specifications are lying about compatibility.  What a letdown, I expected to be fighting issues like MySQL on 64bit Vista but instead I have no POST.  Luckily my brother recently bought very similar Intel hardware so I should be able swap parts around to find out which piece is dead.  As much as it's a badge of geek honor to build one's own screaming development PC, I think in the future I will not do so during times like now where I have plenty of other things to worry about.



Friday, March 14, 2008 10:53:05 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [1]  |  Trackback

My sister had a little boy at 9pm last night, Daniel James Sapp.  I'm an uncle for the 3rd time in a short while.



Friday, March 14, 2008 10:44:02 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Thursday, March 13, 2008

I pulled the trigger on a new desktop setup which is out for delivery according to the internets.  Because of my recent research I went ahead and went the quad core route, and after reading Scott Hanselman's blog post about going x64 being a complete non issue I'll be giving Vista x64 a try.  I also got a 1080p monitor and a Blu-Ray drive; probably slightly overkill but why not.  It's difficult to explain to non-programmers, but an appropriate setup with the right desk space, music, multiple monitors, and a repsonsive machine can make developing software a joy.  It can be a joy without a Raptor drive and a 24" display but these things help. 

I got a decent enough video card to check out some of the recent games, Bioshock being at the top of the list.



Thursday, March 13, 2008 8:19:39 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, March 07, 2008

Catastrophic failure!  Now that is an error message worthy of my college dating life!... anyhoo, I digress.  Many controls in winforms and WPF probably try to do things in the constructor or an "OnLoad" event of some kind that are not design-time-friendly, but the magic of the environment mostly keeps things like this from happening.  This magic dust has not yet been sprinkled upon the Silverlight 2.0 tools for VS2008, but I'm still very excited about the possibilities as this platform matures.  The error below occurs if I add some test images into the loaded event of the carousel and try to view the designer. 



Friday, March 07, 2008 8:23:31 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback

Here's my carousel sample, as a user control within my Silverlight image uploader. The Tesla Roadster is such a sexy car it's perfect for any automotive industry technology demonstration.



Friday, March 07, 2008 8:07:43 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback

Ok, here is my silverlight 2 port of the ever popular Image Carousel sample.  For me, at least, it's much easier to follow what's going on than the JavaScript version.  Here's the markup required in the page:

<UserControl x:Class="ImageUploader.ImageCarousel"

    xmlns="http://schemas.microsoft.com/client/2007"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    Width="800" Height="600" Canvas.Top="0">   

<Canvas x:Name="_mainCanvas" Loaded="mainCanvasLoaded" MouseMove="whenMouseMoves"

        MouseLeftButtonDown="mainDown" MouseLeftButtonUp="mainUp" MouseLeave="mainCanvasMouseLeave" >

    <Canvas Name="imagesHolder" Canvas.Left="0" Canvas.Top="0">

        <Canvas Name="mainImageHolder" Canvas.Left="200" Canvas.Top="60" Canvas.ZIndex="169" Opacity="1.0">

            <Image Name="mainImage" Stretch="UniformToFill" Height="225" Width="300"/>

        </Canvas>

    </Canvas>

</Canvas>

</UserControl>

And here is the code-behind:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Media.Imaging;

namespace ImageUploader
{
public partial class ImageCarousel : UserControl
{
public ImageCarousel()
{
InitializeComponent();
_mainDown = false;
_urls = new Dictionary<Image, string>();
_angles = new List<double>();
_imagePaths = new List<string>();
//_imagePaths.Add("Upload/1.jpg");
//_imagePaths.Add("Upload/2.jpg");
//_imagePaths.Add("Upload/3.jpg");
_dispTimer = new System.Windows.Threading.DispatcherTimer();
_dispTimer.Interval = TimeSpan.FromMilliseconds(25);
_dispTimer.Tick += new EventHandler(dt_Tick);
_dispTimer.Stop();
}

private List<double> _angles;
private bool _mainDown = false;
private List<string> _imagePaths;
private List<ScaleTransform> _scaleTransforms = new List<ScaleTransform>();
private double _speed = -0.0425;
private Dictionary<Image, string> _urls;
private System.Windows.Threading.DispatcherTimer _dispTimer;

public void AddImage(string imageUrl)
{
_mainDown = false;
_dispTimer.Stop();
_angles.Clear();
_scaleTransforms.Clear();
_urls.Clear();

for (int i = 0; i < _imagePaths.Count; ++i)
{
Canvas holder = (Canvas)_mainCanvas.FindName("imgHolder" + i);
_mainCanvas.Children.Remove(holder);
}

_imagePaths.Add(imageUrl);
Image img = (Image)_mainCanvas.FindName("mainImage");
string srcPath = _imagePaths[_imagePaths.Count -1 ];
img.Source = new BitmapImage(new Uri(GetURLBase() + srcPath));
img.SetValue(Canvas.ZIndexProperty, 169);

BuildImages();
}

private void mainCanvasLoaded(object sender, RoutedEventArgs e)
{
if (_imagePaths.Count > 0)
{
Image img = (Image)_mainCanvas.FindName("mainImage");
img.Source = new BitmapImage(new Uri(GetURLBase() + _imagePaths[0]));
img.SetValue(Canvas.ZIndexProperty, 169);
//
BuildImages();
}
}

public List<string> ServerImagePaths { get; set; }

protected void BuildImages()
{
int leftPos = 0;
for (int i = 0; i < _imagePaths.Count; ++i)
{
Canvas imgHolder = new Canvas();
imgHolder.SetValue(Canvas.NameProperty, "imgHolder" + i);
imgHolder.SetValue(Canvas.LeftProperty, leftPos);
imgHolder.SetValue(Canvas.TopProperty, 0);
//Come back to reflections...
//Canvas imgHolderReflection = new Canvas();
//imgHolderReflection.SetValue(Canvas.NameProperty, "imgHolderRef" + i);
//imgHolderReflection.SetValue(Canvas.TopProperty, 160);
//imgHolderReflection.Opacity = 1;
//LinearGradientBrush lgb = new LinearGradientBrush();
//lgb.StartPoint = new Point(0, 0);
//lgb.EndPoint = new Point(0, 1);
//lgb.GradientStops.Add(new GradientStop());
//lgb.GradientStops.Add(new GradientStop());
//lgb.GradientStops[0].Offset = .37;
//lgb.GradientStops[0].Color = Color.FromArgb(0x00, 0x00, 0x00, 0x00);
//lgb.GradientStops[1].Offset = 1;
//lgb.GradientStops[1].Color = Color.FromArgb(0x88, 0x00, 0x00, 0x00);
//imgHolderReflection.OpacityMask = lgb;
//imgHolder.Children.Add(imgHolderReflection);
//
Rectangle rec = new Rectangle();
rec.SetValue(Rectangle.NameProperty, "rec" + i);
rec.SetValue(Canvas.TopProperty, -6);
rec.SetValue(Canvas.LeftProperty, -6);
rec.Height = 92.0;
rec.Width = 92.0;
rec.Fill = new SolidColorBrush(Colors.LightGray);
imgHolder.Children.Add(rec);
//
Image img = new Image();
img.Cursor = Cursors.Hand;
img.MouseEnter +=new MouseEventHandler(img_MouseEnter);
img.MouseLeave +=new MouseEventHandler(img_MouseLeave);
img.MouseLeftButtonDown +=new MouseButtonEventHandler(img_MouseLeftButtonDown);
img.MouseLeftButtonUp +=new MouseButtonEventHandler(img_MouseLeftButtonUp);
img.Stretch = Stretch.UniformToFill;
img.SetValue(Image.NameProperty, "img" + i);
BitmapImage imgSource = new BitmapImage(new Uri(GetURLBase() + _imagePaths[i]));
_urls.Add(img, GetURLBase() + _imagePaths[i]);
img.Source = imgSource;
img.Width = 300;
img.Height = 225;

img.Opacity = 1;
imgHolder.Children.Add(img);
ScaleTransform imgCanvasScaleTrans = new ScaleTransform();
_scaleTransforms.Add(imgCanvasScaleTrans);
imgCanvasScaleTrans.ScaleX = 1;
imgCanvasScaleTrans.ScaleY = 1;
imgCanvasScaleTrans.CenterX = 50;
imgCanvasScaleTrans.CenterY = 50;
imgHolder.RenderTransform = imgCanvasScaleTrans;
//
_mainCanvas.Children.Add(imgHolder);
//
_angles.Add( i*((Math.PI*2)/_imagePaths.Count) );
}
PositionItems();
_mainDown = true;// huh?
//Set dispatcher interval
_dispTimer.Start();
}

/// <summary>
/// Not used yet
/// </summary>
/// <param name="i"></param>
protected void AddOne(int i)
{
}

void dt_Tick(object sender, EventArgs e)
{
MoveItems();
}

protected void PositionItems()
{
int radiusX=400;
int radiusY=110;
int centerX=425;
int centerY = 260;

for(int i = 0; i < _imagePaths.Count;++i)
{
double myX = Math.Cos(_angles[i])*radiusX + centerX;
double myY = Math.Sin(_angles[i])*radiusY + centerY;
Canvas imgCanvas = (Canvas)_mainCanvas.FindName("imgHolder" + i);
imgCanvas.SetValue(Canvas.LeftProperty, myX);
imgCanvas.SetValue(Canvas.TopProperty, myY);
ScaleTransform stRef = _scaleTransforms[i];
double sc = (myY - stRef.ScaleY) / (centerY + radiusY-stRef.ScaleY);
stRef.ScaleX = sc;
stRef.ScaleY = sc;
_angles[i] += _speed;
imgCanvas.SetValue(Canvas.ZIndexProperty, (int)myY);
}
}

protected void MoveItems()
{
if (_mainDown)
{
PositionItems();
}
}

void img_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{

}

void img_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Image img = (Image)sender;

     Image mainImg = (Image)_mainCanvas.FindName("mainImage");
mainImg.Source = new BitmapImage(new Uri(_urls[img]));
mainImg.SetValue(Canvas.ZIndexProperty, 169);
}

void img_MouseLeave(object sender, MouseEventArgs e)
{
//Change color BACK or something cool
}

void img_MouseEnter(object sender, MouseEventArgs e)
{
//Change color or something cool
}

private void whenMouseMoves(object sender, MouseEventArgs e)
{
Point pt = e.GetPosition(null);
double _root_xmouse = pt.X;
     double _root_ymouse = pt.Y    ;
_speed = ((_root_xmouse - 500) / 500) * 0.0755;
}

private void mainDown(object sender, MouseButtonEventArgs e)
{
_mainDown = true;
}

private void mainUp(object sender, MouseButtonEventArgs e)
{
_mainDown = false;

}

private void mainCanvasMouseLeave(object sender, MouseEventArgs e)
{

}


protected string GetURLBase()
{
string str = System.Windows.Application.Current.Host.Source.OriginalString;
return str.Substring(0, str.LastIndexOf("/")).Replace("ClientBin", string.Empty);
}
}
}


 

Not formatted as well as my last posts, I know.  It seems that Google reader doesn't like it when I paste out of Word. Now, one thing that is not very apparent is how to add this UserControl to another UserControl, assuming that JUST having an image carousel is not what you're after.  You need to include the following on the top-level user control:

xmlns:CarSpot="clr-namespace:ImageUploader"

which then allows you to refer to the code:

<CarSpot:ImageCarousel x:Name="_carousel"></CarSpot:ImageCarousel>

This is an aspect of XAML I don't like. There's nothing (that I can see) in the structure/XSD of a XAML document that tells me that an attribute value starting with "clr-namespace:" is something special.  There's a lot of magic like this in XAML.  The Visual Studio "Silverlight Chainer" as the file calls the Beta 1 tools are definately buggy and incomplete.  I am lucky to be able to hit F5 10 times before I get errors trying to debug and having to kill Cassini and VS2008.  I've also found that it's very easy to crash the designer by having naughty things in your code-behind.  It appears that unlike more mature design-time experiences there is no "IsDesignTime" flag that would allow you to keep from having code execute while designing.  I can tell that this is Cider by the wonderfully descriptive stack traces I get, yet things like dragging the new Silverlight controls onto the surface or even moving them around (I'm sick of tweaking location with Canvas.Top/Canvas.Left) are not working yet.  This is a huge step forward, but I've got to think there'll be another beta after this one now.

And that's that.  I should have some more Silverlight 2 observations later, especially about the threading and HTTP aspects.



Friday, March 07, 2008 4:36:07 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [4]  |  Trackback

I have translated the Silverlight "Image Carousel" into Silverlight 2.0 and C# rather than the "JS writing XAML" model.  If I get the code cleaned up this afternoon I will post the carousel portion.



Friday, March 07, 2008 1:33:00 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, March 05, 2008
IE8

I am blogging from IE8.

Many useful things are broken, such as FreeTextBox which I am used to using to write this article.

Also broken, ironically enough, are the dynamic menus generated by asp.net from a sitemap data source. That alone will force me into "Emulate IE7" mode until Beta 2...



Wednesday, March 05, 2008 4:06:34 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [2]  |  Trackback