tag:blogger.com,1999:blog-2406184615005531302024-03-13T19:40:44.353+11:00Dev thoughtsMy thoughts from a developer/architect point of view, covering any technology I get my hands on.Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.comBlogger19125tag:blogger.com,1999:blog-240618461500553130.post-51684882699940137632014-06-20T10:23:00.000+10:002014-06-20T10:23:39.987+10:00Friend Request in JavaScript using Parse.com - Part 3Now for actions, assuming you added some nice little buttons to those lists.<br />
<br />
These are very simplistic, just to show the concept. You would probably want to have some Cloud Code that will verify things in a before-save function. Also all my examples haven't included any error handling but you should add some.
<h3>Approve a request</h3>
<pre class="prettyprint">// assuming you saved the FriendRequest.id as maybe an attribute
// data-friend-request-id="xxxx" on the Approve button this code is
// linked to
var friendRequest = new FriendRequest();
friendRequest.id = $(this).data('friendRequestId');
friendRequest.set('status', RequestStatus.approved);
friendRequest.save();
</pre>
<h3>Reject a request</h3>
As per Approve, but use <pre class="prettyprint">friendRequest.set('status', RequestStatus.rejected);</pre>
<h3>Un-friend</h3>
As per Approve, but use <pre class="prettyprint">friendRequest.set('status', RequestStatus.rejected);</pre>
Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com1tag:blogger.com,1999:blog-240618461500553130.post-30513434893922320022014-06-20T10:12:00.001+10:002014-06-20T10:12:48.557+10:00Friend Request in JavaScript using Parse.com - Part 2Continued from Part 1, our next requirement is:<br />
<br />
<h3>
List requests from others waiting for my approve/reject action</h3>
<pre class="prettyprint">// remember our common code, FriendRequest, currentUser
// and RequestStatus are already defined
var pendingRequestQuery = new Parse.Query(FriendRequest);
pendingRequestQuery.equalTo('toUser', currentUser);
pendingRequestQuery.equalTo('status', RequestStatus.requested);
requests.find().then(function (requests) {
// render them somewhere with Approve/Reject buttons
});
</pre>
I'll leave it as an exercise for the reader to render the list.<br />
<br />
<h3>List my friends</h3>
<pre class="prettyprint">var myFriendsQuery1 = new Parse.Query(FriendRequest);
myFriendsQuery1.equalTo('fromUser', currentUser);
myFriendsQuery1.equalTo('status', RequestStatus.approved);
var myFriendsQuery2 = new Parse.Query(FriendRequest);
myFriendsQuery2.equalTo('toUser', currentUser);
myFriendsQuery2.equalTo('status', RequestStatus.approved);
var myFriendsQuery = Parse.Query.or(myFriendsQuery1, myFriendsQuery2);
myFriendsQuery.find().then(function (approvedFriendRequests) {
// render them
});
</pre>
<h3>List my requests that are pending</h3>
<pre class="prettyprint">var pendingRequestQuery = new Parse.Query(FriendRequest);
pendingRequestQuery.equalTo('fromUser', currentUser);
pendingRequestQuery.equalTo('status', RequestStatus.requested);
pendingRequestQuery.find().then(function (pendingRequests) {
// render them
});
</pre>
<h3>List my rejections</h3>
<pre class="prettyprint">var rejectedMeQuery = new Parse.Query(FriendRequest);
rejectedMeQuery.equalTo('fromUser', currentUser);
rejectedMeQuery.equalTo('status', RequestStatus.rejected);
rejectedMeQuery.find().then(function (rejectedMeRequests) {
// render them
});
</pre>
<h3>List requests I have rejected</h3>
<pre class="prettyprint">var rejectedByMeQuery = new Parse.Query(FriendRequest);
rejectedByMeQuery.equalTo('toUser', currentUser);
rejectedByMeQuery.equalTo('status', RequestStatus.rejected);
rejectedByMeQuery.find().then(function (rejectedByMeRequests) {
// render them
});
</pre>
In the next part I'll talk about Actions.Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com0tag:blogger.com,1999:blog-240618461500553130.post-46025264075420102362014-06-20T09:49:00.002+10:002014-06-20T09:49:26.264+10:00Friend Request in JavaScript using Parse.com - Part 1This is a follow-on from my last post, almost a year ago. Yes, I know, I should post more often, anyway...<br />
<br />
First off we need to define some things that are common to all the code blocks:
<br />
<pre class="prettyprint">// fake an enum, allows auto-complete for some editors
var RequestStatus = {
requested: 'requested',
rejected: 'rejected',
approved: 'approved'
};
// define our Parse Class
var FriendRequest = Parse.Object.extend('FriendRequest');
var currentUser = Parse.User.current();</pre>
I will also assume you have jQuery available.<br />
<br />
Now lets go through our requirements.
<br />
<h3>
Find a person (highlight existing status)</h3>
<pre class="prettyprint">// for simplicity lets assume we're doing a search for people
// in the same "area" as us, lets also assume you're aware of
// case-sensitive issues and created a "search_Area" property
// forced to lower-case (using a before-save Cloud Function)
var userQuery = new Parse.Query(Parse.User);
userQuery.equalTo('search_Area', currentUser.get('search_Area'));
// shared array we'll store details in
var userList = [];
// dictionary we'll use for easier updating
var userDict = [];
userQuery.find().then(function(users) {
// lets just extract the "id" and "username" first
for (var i in users) {
var userInfo = {
id: users[i].id,
name: users[i].get('username')
});
userList.push(userInfo);
userDict[userInfo.id] = userInfo;
}
// now we need to attach a status to each one
var myRequests = new Parse.Query(FriendRequest);
myRequests.equalTo('fromUser', currentUser);
myRequests.equalTo('toUser', users);
var requestsToMe = new Parse.Query(FriendRequest);
requestsToMe.equalTo('fromUser', users);
requestsToMe.equalTo('toUser', currentUser);
var combinedRequests = Parse.Query.or(myRequests, requeststoMe);
return combinedRequest.find();
}).then(function (requests) {
for (var i in requests) {
var request = requests[i];
var status = request.get('status');
if (request.get('fromUser').id == currentUser.id) {
// set status from my perspective
userDict[request.get('toUser').id].statusMessage =
status == RequestStatus.approved ? 'Already my friend' :
status == RequestStatus.rejected ? 'I rejected them' :
status == RequestStatus.requested ? 'Waiting for them to reply' :
'Unknown status';
} else {
// set status from the other person's perspective
userDict[request.get('fromUser').id].statusMessage =
status == RequestStatus.approved ? 'Already my friend' :
status == RequestStatus.rejected ? 'They rejected me' :
status == RequestStatus.requested ? 'Waiting for me to reply' :
'Unknown status';
}
}
// at this point you can render the userList
for (var i in userList) {
var userInfo = userList[i];
// assuming you have a container DIV with id="possibleFriends"
$('#possibleFriends').append(
'<div data-user-id="' + userInfo.id + '"'
+ ' class="possibleFriend">'
+ userInfo.userName
// not all will have a status, so handle that
+ (userInfo.status ? ' - ' + userInfo.status : '')
+ '</div>');
}
});
</pre>
With the above you can extend it however you like to allow sorting etc, it is just the bare-bones implementation.Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com0tag:blogger.com,1999:blog-240618461500553130.post-23912240108606020692013-09-25T12:40:00.002+10:002013-09-25T12:40:55.917+10:00A parse.com tutorial - implementing a friends listI've been playing with Parse in my spare time recently, and a couple of topics seem to come up quite often: relationships and some kind of "friends list" functionality.<br />
<br />
A "friends list" is a great example of a many-to-many relationship. Seems to me that a tutorial of how to add a "friends list" would solve both these questions, so that's what I'm doing.<br />
<h2>
Requirements</h2>
First lets think of what a "friends list" should entail:<br />
<ul>
<li>Find a person</li>
<li>Add them to my friends list</li>
</ul>
<div>
That's it! We're done, right? Well often you want to allow people to approve/reject being friended. You'll want to have the friendship be both ways (if I add you to my list, then you get to add me to yours). There's all sorts of extra things to consider.</div>
<div>
<h2>
Revised Requirements</h2>
</div>
<div>
Here's an expanded list, lists first:</div>
<div>
<ul>
<li>Find a person</li>
<ul>
<li>Highlight if I already have them in my friends list, or have asked</li>
</ul>
<li>List requests from others waiting for my approve/reject action</li>
<li>List my friends</li>
<li>List my requests that are pending</li>
<li>List my rejections</li>
<li>List requests I have rejected</li>
</ul>
<div>
Now for actions we want to be able to perform:</div>
<div>
<ul>
<li>Request to add someone as a friend</li>
<li>Approve a request</li>
<li>Reject a request</li>
<li>Un-friend</li>
<li>Things we could add later:</li>
<ul>
<li>Un-reject (change to approved, in case someone makes a mistake)</li>
<li>Re-request (possibly with a time-window to prevent abuse)</li>
</ul>
</ul>
</div>
<div>
We will leave off things like showing how many friends you have in common etc.</div>
</div>
<div>
<br /></div>
<div>
Given these requirements, a Parse class like the following should give us what we want:</div>
<div>
<br /></div>
<div>
Class: FriendRequest</div>
<div>
RequestFrom (reference: User)</div>
<div>
RequestTo (reference: User)<br />
Status (string: requested/approved/rejected/etc)<br />
<br />
So, lets see how we can handle the above lists and actions:<br />
<h3>
Find a person (highlight existing status)</h3>
Once your existing "find a person" query is run, you would need an array of User objects. Using this list we could ask for all FriendRequest objects where RequestFrom is in the list or RequestTo is in the list.<br />
<h3>
List requests from others waiting for my approver/reject action</h3>
</div>
<div>
Query FriendRequest where the current user is the RequestTo and the status is "requested".</div>
<h3>
List my friends</h3>
<div>
Query FriendRequest where the current user is RequestFrom or RequestTo and the status is "approved".</div>
<h3>
List my requests that are pending</h3>
<div>
Query FriendRequest where the current user is RequestFrom and the status is "requested".</div>
<h3>
List my rejections</h3>
<div>
<div>
Query FriendRequest where the current user is RequestFrom and the status is "rejected".</div>
</div>
<h3>
List requests I have rejected</h3>
<div>
<div>
Query FriendRequest where the current user is RequestTo and the status is "rejected".</div>
</div>
<div>
<br /></div>
<div>
Well, looks like we can handle all the requests, what about the actions?<br />
<h3>
Request to add someone as a friend</h3>
First make sure there's no existing record with the current User and the target User as RequestFrom/RequestTo or the other way around.<br />
<br />
If that requirement is met then you just need to create a new FriendRequest object with the following properties:<br />
<pre>RequestFrom: current User
RequestTo: target User
Status: requested</pre>
<h3>
Approve a request</h3>
Change the Status from "requested" to "approved".<br />
<h3>
Reject a request</h3>
Change the Status from "requested" to "rejected".<br />
<h3>
Un-friend</h3>
</div>
<div>
Change the Status from "approved" to "rejected".<br />
<h2>
Summary</h2>
In theory it looks like everything is covered, though I welcome comments and feedback if you think I'm missed anything or made a mistake.<br />
<br />
I'll post some sample code (JavaScript and maybe some Objective-C too) once I get my code formatting working again.</div>
Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com3tag:blogger.com,1999:blog-240618461500553130.post-49930388924848397892013-09-25T06:51:00.001+10:002013-09-25T06:51:34.133+10:00Things I'm playing with nowWow, it's been a while since I've updated this blog. Looks like my code-formatting broke too.<br />
<br />
Anyway, lots of things have changed, but here's what I am looking at recently:<br />
<ul>
<li><a href="http://parse.com/" target="_blank">Parse.com</a>, a data backend with SDKs for all major platforms: JavaScript, iOS, Andriod, .NET as well as REST</li>
<li>LINQ, specifically <a href="http://www.albahari.com/nutshell/predicatebuilder.aspx" target="_blank">PredicateBuilder</a> that lets you build OR logic, as well as building expressions and having them translated into SQL calls</li>
</ul>
<div>
I'll see if I can post some tutorials on using Parse some time soon.</div>
Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com0tag:blogger.com,1999:blog-240618461500553130.post-91114979693613914672009-10-29T17:14:00.001+11:002009-10-29T17:14:04.524+11:00Running Windows 7 in a VM on OS X 10.6<p>Anyone that’s worked with me recently will know that I’m a “switcher”, someone who has changed from being a Mac hater / Windows lover to a more realistic lover of what’s good in both OS’s (it helps that Mac released OS X, as most of my pain was with OS 7-9 clients on a Windows NT network). That said all the computer in my house are Macs, except for an old Toshiba T200 TabletPC that my wife uses to play games with the kids. This gives me the ability to run any operating system I like on all the computers.</p> <p>It should come as no surprise then that I eagerly tried Windows 7 back when the first beta was made available to the public. I chose to run it in Boot Camp, mostly because I was testing how well my games ran, since I had been running them in XP. When I was doing “serious” stuff (Visual Studio 2008, SQL etc) I mostly ran in a VM in Mac mode, as it gave me greater flexibility.</p> <p>After my terrible experience with Vista (twice it became unrecoverable, causing me a lot of problems) I went cautiously, but was pleasantly surprised to find that Windows 7 (even as Beta and then RC) was pretty stable, quite responsive, and ran everything rather well.</p> <p>My only real pain point was having to reboot into Boot Camp for anything that required decent graphics, and to get the full Aero experience.</p> <p>To my great delight VM Ware and Parallels recently release new versions of their VM tools, I quickly tried both, as I have purchased previous versions of both and found each to have nice features.</p> <h2>Parallels 4</h2> <p>I was disappointed to discover no DirectX support in the latest offering, unless I missed the feature somewhere. That said, the features continue to improve and it feels more solid.</p> <h2>VM Ware Fusion 3</h2> <p>This release is huge, they’ve changed a lot and I’m quite impressed. I’ve so far tried it on a 2009 Mac Book Pro 13”, and a iMac (late 2007 model I think, with Core 2 Extreme 2.8). Both ran it quite well, Windows 7 works with Aero, and yes even games seem to work well, although given that those two computers have limited graphics power to start with I wasn’t expecting much.</p> <p>Once I can convince my wife to get off my Mac Pro for long enough I’ll test the Direct 3D support on there, since with 8 cores and 6Gb RAM with a 9800GT card I expect to be able to get good performance even in a VM.</p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com1tag:blogger.com,1999:blog-240618461500553130.post-28664908283779779302009-10-14T18:03:00.001+11:002009-10-14T18:03:58.982+11:00First week at a new job<p>Well I’ve started a new contract this week, but this time I’m not inflicted with draconian web filtering. Maybe now I’ll be able to do some more blogging, posting little tid-bits of information that seem interesting and get back in touch with some of my peers online. </p> <p>I was with ABB Grain (now a yet-to-be-named part of Viterra, perhaps Viterra Australia?) for around 15 months, in that time I assembled a team of great developers and as a team we produced a great product that our users love. Thanks to everyone there that made those 15 months so memorable for me.</p> <p>I’ve discovered that I can even run MSN Messenger now, but we’re under a pretty tight deadline so I doubt I’ll have time to chat or answer questions for the next couple of weeks, so I won’t bother signing in until things calm down a bit.</p> <p>Oh, Ben Laan, this means you have to do your blog post now :P</p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com1tag:blogger.com,1999:blog-240618461500553130.post-12112554271208500052008-07-21T08:15:00.001+10:002008-07-22T17:56:01.999+10:00Time Tracker - a demo Sync application<p>I've created a little demo application now that I've had a chance to get some better real-world experience with creating a <a href="http://msdn.microsoft.com/sync/" target="_blank">Microsoft Sync Framework for ADO.NET</a> application.</p> <p>I feel that the first rule of creating an application that will work in offline mode using Sync is to get your basic application working without sync, talking to a local CE database. It's important to keep in mind all the usual concurrency issues involved with creating a database application such as:</p> <ul> <li>multiple users updating the same row </li> <li>updating something that someone else deleted </li> <li>deleting something that someone else updated </li> <li>etc... </li> </ul> <p>These issues become even more of a problem when using Sync since the time between the user pressing "Save" and when it actually gets written to the central database can be a long time if they're offline. Also due to this disconnected nature you'll need to handle it at the server, which means you might want to create a "conflict" table to store any client-side changes that are going to be rejected so that user can be notified that their change failed for some reason and given the chance to merge or retry their update.</p> <p>In this demo app I'll keep it simple, we'll assume that whoever makes the last change to a row wins, since each person should only be updating their own time-sheet.</p> <p>Our app flow is as follows:</p> <ol> <li>Pick an employee from a list </li> <li>Select a day (date) to add/edit/delete entries for </li> <li>Add/edit/delete entries </li> <li>Save changes to this day's entries </li> <li>Go back to (2) and pick another day or exit (you could also select another employee if you want and start again) </li> </ol> <p>You'll notice that there's no ability to do any updates to the list of Employees, Projects or BillingInfos (list of billing codes), these are considered management tasks and should be done by a separate application (which I might build later if people are interested).</p> <p>I like to have a "Splash Window", especially since .NET (and WPF even more so) applications can take a while to start, and it'll come in handy later when we'll want to deal with checking the local DB, possibly showing a config window, or even asking for a login.</p> <p>So, our app is very simple:</p> <ul> <li>SplashWindow - simple window to let people know the app is starting, will be handy later for other things </li> <li>TimeSheetWindow - where everything happens </li> <li>LocalDatabase.sdf - SQL CE database with our 4 tables </li> <li>CreateLinqToSql.cmd – a batch file to generate the LINQ to SQL classes </li> <li>Data\LocalDatabase.cs – generated code for LINQ to SQL </li> <li>Data\LocalDataContext.cs – partial class, uses connection string from config </li> <li>Data\Employee.cs – partial class, added a FullName property </li> <li>Data\TimeEntry.cs – partial class, sets ID to a new GUID when created </li> </ul> <p>You’ll notice the user experience isn’t very satisfying and data validation is quite minimal, but it’s enough for this sample. One possible way to improve it is to use the Xceed DataGrid for WPF (free edition), which also includes a nice DatePicker control.</p> <p>You can download the application from <a href="http://code.google.com/p/timetrackerdemo/" target="_blank">Time Tracker Demo</a> on Google Code, if you have TortoiseSVN or similar you'll be able to easily download the code and try it out, this version is branch “Stage1”.</p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com0tag:blogger.com,1999:blog-240618461500553130.post-83588158769221638822008-07-17T17:02:00.001+10:002008-07-17T17:02:06.517+10:00Using SQL Express (or Standard/Enterprise) at the Client<p>First off, why would anyone want to use SQL Express (or one of it's bigger brothers) at the client? For us there's the case of a Site (grain silo) that wants offline functionality (if the link to head-office goes down), but still have changes on one PC be visible on all others. This is a perfect case for all the apps at that site to talk to a local SQL Server, and have a service running on the server that will sync with head-office when the connection is up.</p> <p>We took a look at the <a href="http://blogs.msdn.com/sync/archive/2008/06/24/sample-sql-express-client-synchronization-using-sync-services-for-ado-net.aspx">SQL Express Client Synchronization using Sync Services for ADO.NET</a> sample provided, which was very helpful. Some of the coding standards were not quite how I would have liked, but as a proof-of-concept it did the job.</p> <p>After a few false starts we finally got it working in our trial application. There's one very interesting issue that we found: <strong>all your tables in your SyncAgent must be defined as BiDirectional</strong>, otherwise the SqlExpressClientSyncProvider sample class ignores them!</p> <p>I was concerned that this might allow changes to download only tables, but if your server provider (which in our trial app is inside a WCF service) doesn't set the UpdateCommand or DeleteCommend then these changes are silently swallowed by the server. The client thinks the changes have been made server-side, but the server doesn't actually do anything.</p> <p>This represents the final stage in proving our architecture to management, so now we'll be going full-steam ahead next week, with two new staff members starting on Monday: Nigel Spencer and Richard Hollon. That leaves only one person left for our "dream team" who will be starting in a few weeks.</p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com3tag:blogger.com,1999:blog-240618461500553130.post-35455223125433213612008-07-02T22:31:00.001+10:002013-09-25T21:02:37.464+10:00Sync for ADO.NET CTP2 - Batch Anchor proc is broken?In the documentation for the CTP2 it gives a sample stored procedure called <strong>usp_GetNewBatchAnchor</strong>, but from my tests it doesn't seem to work.<br />
I had to make a few changes in my version:<br />
<ul>
<li>parameters are defined as "timestamp" data type, changed them to "binary(8)"</li>
<li>for consistency I changed the "out" suffix on 2 of the 3 output params to be "output" like the 3rd one</li>
<li>multiple checks of "IF @sync_batch_count <= 0" need to be replaced with "IF @sync_batch_count IS NULL OR @sync_batch_count <= 0"</li>
</ul>
NOTE: you'll also need to change the tables it's using to check for the min timestamp value, in my case the following:<br />
<pre class="brush: sql">SELECT @sync_last_received_anchor = MIN(TimestampCol)
FROM
(
SELECT MIN(CreateVersion) AS TimestampCol FROM dbo.BillingInfo
UNION
SELECT MIN(UpdateVersion) AS TimestampCol FROM dbo.BillingInfo
UNION
SELECT MIN(CreateVersion) AS TimestampCol FROM dbo.Employees
UNION
SELECT MIN(UpdateVersion) AS TimestampCol FROM dbo.Employees
UNION
SELECT MIN(CreateVersion) AS TimestampCol FROM dbo.Projects
UNION
SELECT MIN(UpdateVersion) AS TimestampCol FROM dbo.Projects
UNION
SELECT MIN(CreateVersion) AS TimestampCol FROM dbo.TimeEntries
UNION
SELECT MIN(UpdateVersion) AS TimestampCol FROM dbo.TimeEntries
UNION
SELECT MIN(DeleteVersion) AS TimestampCol FROM dbo.TimeEntries_Tombstone
) MinTimestamp</pre>
<br />
<br />
Before I made the above changes I was getting erratic behaviour depending on the batch size specified. Now everything seems to be working as expected.Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com4tag:blogger.com,1999:blog-240618461500553130.post-31836451115572695562008-07-02T21:28:00.001+10:002008-07-02T21:28:17.210+10:00Sync for ADO.NET - Resolving Conflicts<p>I've been spending my spare time for the last few days looking into how to resolve conflicts. The samples in the help files with CTP2 are (IMHO) missing a key bit of info: the ApplyChangeFailed event on the SqlCeClientSyncProvider may not be what some expect, it is only raised if a change is made to a record after the upload phase but before a server change is downloaded and applied. </p> <p>On slower connections that aren't using batching this can happen more often since the time from upload to commit of the "apply server changes" stage is longer.</p> <p>I admit that I was expecting the following:</p> <ol> <li>A data row exists on the server (lets call it ID:1, Label:Original, Version:1000)</li> <li>Client A downloads it and makes a change (ID:1, Label:ClientA)</li> <li>Client B downloads it and makes a change (ID:1, Label:ClientB)</li> <li>Client A syncs (last anchor 1000), server saves new version (ID:1, Label:ClientA, Version:1001)</li> <li>Client B syncs (last anchor 1000), server provider gets an ApplyChangeFailed event, we choose to Continue (basically saying Server copy wins)</li> <ol> <li>I was <strong>EXPECTING</strong> the client to then raise an ApplyChangeFailed event so I could tell the user that their change has a conflict with the server (record 1001), and allow them to pick one or merge</li> <li>What <strong>actually happens</strong> is that version 1001 is downloaded and replaces our change, destroying it forever</li> </ol> </ol> <p>So, what can we do here? What we need to do is in the Server Provider decide what to do with the conflicting rows. If we want to keep the first change (saved as version 1001) we need to write the change from Client B somewhere else, such as <TableName>_Conflicts, including a note of the ClientID that caused the conflict. If this table is made part of our sync then that client (or all clients, depending on your needs) will now have it's change in the <TableName>_Conflicts table and the version from Client A in the <TableName> table. The UI can respond to this and show a conflict resolution dialog, if a merge or replace is decided upon then the <TableName> row is updated (and will be part of the next sync) and the <TableName>_Conflicts row will be deleted (and will be removed from the server next sync).</p> <p>A special thanks goes out to <a href="http://www.paulstovell.com/blog/">Paul Stovell</a> for helping me discover where my expectation and the actual implementation differed.</p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com1tag:blogger.com,1999:blog-240618461500553130.post-15510511421066404192008-06-29T08:46:00.001+10:002008-06-29T08:46:23.476+10:00Sync for ADO.NET - Client (Part 2) - DbServerSyncProvider<p>While preparing the code for this part I tried to use the SqlSyncAdapterBuilder, which works great if you want to do Download Only or Snapshot, but if you want to do Bidirectional and you don't have a separate tombstone table you're out of luck. At this stage I have a choice to make, I either change my database schema to use a tombstone table, write all the SQL myself, or tinker with the SqlSyncAdapterBuilder and see if I can at least get it to do the bulk of the work.</p> <ul> <li>Change database schema: <ul> <li>A tool or framework you use shouldn't force you to alter your design as a general rule, the framework is happy with my planned design but the helper class doesn't support it </li> <li>At this stage it's not a big deal to change the schema, and it has the added benefit of moving inactive rows or virtual deletions out of the main table, making them easier to manage and find </li> <li>If I need to show inactive rows on the client I'll need to get creative (like sync the tombstone table as a download-only table without the extra columns) </li> </ul> </li> <li>Write all the SQL myself <ul> <li>I'm planning to do this eventually, wrapping it all up in stored procedures. In a production environment this is the best option as it allows DB logic to be managed easily and adjusted as required if changes are made to the backing store that don't require UI changes </li> <li>It's not so quick to create a quick prototype or demo, but it's a better long-term option </li> </ul> </li> <li>Tinker with the SqlSyncAdapterBuilder <ul> <li>I love to tinker with code, but I expect the end result will be letting it create an adapter for download only and then changing it and writing the delete commands myself </li> </ul> </li> </ul> <p>End result? At this stage of development we're after quick results and a good demo, so I'll modify the schema. When we do change to using stored procedures I can then go back to my old schema and change the procs without having to change any client code at all! That makes my inner-DBA very happy.</p> <p>Making the CustomServerProvider becomes trivial now, but I made 2 static methods to make it easier to read:</p> <pre class="C#" name="code"> private static SyncAdapter GetDownloadOnlyAdapter( SqlConnection cnn, string tableName, string primaryKey )<br /> {<br /> var builder = new SqlSyncAdapterBuilder( cnn )<br /> {<br /> TableName = tableName,<br /> RowGuidColumn = primaryKey,<br /> SyncDirection = SyncDirection.DownloadOnly,<br /> CreationTrackingColumn = "CreateVersion",<br /> CreationOriginatorIdColumn = "CreateID",<br /> UpdateTrackingColumn = "UpdateVersion",<br /> UpdateOriginatorIdColumn = "UpdateID"<br /> };<br /><br /> SyncAdapter adapter = builder.ToSyncAdapter();<br /> adapter.TableName = tableName;<br /> return adapter;<br /> }<br /><br /> private static SyncAdapter GetBidirectionalAdapter( SqlConnection cnn, string tableName, string primaryKey )<br /> {<br /> var builder = new SqlSyncAdapterBuilder( cnn )<br /> {<br /> TableName = tableName,<br /> TombstoneTableName = tableName + "_Tombstone",<br /> RowGuidColumn = primaryKey,<br /> SyncDirection = SyncDirection.Bidirectional,<br /> CreationTrackingColumn = "CreateVersion",<br /> CreationOriginatorIdColumn = "CreateID",<br /> UpdateTrackingColumn = "UpdateVersion",<br /> UpdateOriginatorIdColumn = "UpdateID",<br /> DeletionTrackingColumn = "DeleteVersion",<br /> DeletionOriginatorIdColumn = "DeleteID"<br /> };<br /><br /> SyncAdapter adapter = builder.ToSyncAdapter();<br /> adapter.TableName = tableName;<br /> return adapter;<br /> }</pre><br /><br /><p>I have a few tables I want to sync using DownloadOnly and one using Bidirectional, here's the code:</p><br /><br /><pre class="C#" name="code">SyncAdapters.Add( GetDownloadOnlyAdapter( cnn, "BillingInfo", "BillingInfoID" ) );<br />SyncAdapters.Add( GetDownloadOnlyAdapter( cnn, "Employees", "EmployeeID" ) );<br />SyncAdapters.Add( GetDownloadOnlyAdapter( cnn, "Projects", "ProjectID" ) );<br />SyncAdapters.Add( GetBidirectionalAdapter( cnn, "TimeEntries", "TimeEntryID" ) );</pre><br /><br /><p>I'm happy with the CustomServerProvider class now, so it's time to work on the actual UI next.</p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com1tag:blogger.com,1999:blog-240618461500553130.post-22775002591004815572008-06-26T20:59:00.001+10:002008-06-26T20:59:31.532+10:00Code highlighting workflow, using free tools<p></p> <p>This time I created the below by pasting my code into <a href="http://notepad-plus.sourceforge.net/uk/site.htm">Notepad++</a>, selected all text, and went to TextFX, TextFX Convert, Encode HTML. Lastly I added a <pre class="C#" name="code"> to the top and </pre> to the bottom and copied it all, then used Past Special in Live Writer so I could tell it to paste as HTML.</p> <p>Here's the result:</p> <pre class="C#" style="overflow: auto" name="code"> public CustomServerProvider()<br /> {<br /> var cnn = new SqlConnection( Properties.Settings.Default.ServerConnectionString );<br /> Connection = cnn;<br /><br /> const string newAnchorVariable = "@" + SyncSession.SyncNewReceivedAnchor;<br /> var selectNewAnchorCommand = new SqlCommand<br /> {<br /> CommandText =<br /> string.Format( "SELECT {0} = min_active_rowversion() - 1",<br /> newAnchorVariable ),<br /> Connection = cnn<br /> };<br /> selectNewAnchorCommand.Parameters.Add( newAnchorVariable, SqlDbType.Timestamp ).Direction = ParameterDirection.Output;<br /> this.SelectNewAnchorCommand = selectNewAnchorCommand;<br /><br /> var downloadOnlyTables = new Dictionary<string, string> { { "BillingInfo", "BillingInfoID" }, { "Employees", "EmployeeID" }, { "Projects", "ProjectID" } };</pre><br /><br /><p>Much better in my opinion!</p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com0tag:blogger.com,1999:blog-240618461500553130.post-82404018786746435112008-06-26T07:06:00.001+10:002008-06-26T21:02:02.221+10:00Testing SyntaxHighlighter<p>This post is to check that I've setup <a href="http://code.google.com/p/syntaxhighlighter/">SyntaxHighlighter</a> correctly.</p> <pre class="C#" name="code"> public class CustomServerProvider : DbServerSyncProvider<br /> {<br /> public CustomServerProvider()<br /> {<br /> var cnn = new SqlConnection( Properties.Settings.Default.ServerConnectionString );<br /> Connection = cnn;<br /><br /> const string newAnchorVariable = "@" + SyncSession.SyncNewReceivedAnchor;<br /> var selectNewAnchorCommand = new SqlCommand<br /> {<br /> CommandText =<br /> string.Format( "SELECT {0} = min_active_rowversion() - 1",<br /> newAnchorVariable ),<br /> Connection = cnn<br /> };<br /> selectNewAnchorCommand.Parameters.Add( newAnchorVariable, SqlDbType.Timestamp ).Direction = ParameterDirection.Output;<br /> this.SelectNewAnchorCommand = selectNewAnchorCommand;<br /><br /> var downloadOnlyTables = new Dictionary<string string ,> { { "BillingInfo", "BillingInfoID" }, { "Employees", "EmployeeID" }, { "Projects", "ProjectID" } };<br /><br /> foreach( var pair in downloadOnlyTables )<br /> {<br /> var tableName = pair.Key;<br /> var primaryKey = pair.Value;<br /><br /> var builder = new SqlSyncAdapterBuilder( cnn )<br /> {<br /> TableName = tableName,<br /> RowGuidColumn = primaryKey,<br /> SyncDirection = SyncDirection.DownloadOnly,<br /> CreationTrackingColumn = "CreateVersion",<br /> CreationOriginatorIdColumn = "CreateID",<br /> UpdateTrackingColumn = "UpdateVersion",<br /> UpdateOriginatorIdColumn = "UpdateID"<br /> };<br /><br /> SyncAdapter adapter = builder.ToSyncAdapter();<br /> adapter.TableName = tableName;<br /> this.SyncAdapters.Add( adapter );<br /> }<br /><br /> this.SyncAdapters.Add( TimeEntriesAdapter.GetNew( cnn ) );<br /> }</pre><br /><br /><p>So, there's still some tweaking needed it seems, but it's looking good, perhaps I just need to create a tool to make the code HTML safe, and perhaps wrap it in the right PRE tag for me... something to think about for later</p><br /><br /><pre class="C#:nogutter" name="code">new Dictionary<string string ,></pre><br />vs <br /><br /><pre class="C#:nogutter" name="code">new Dictionary<string , string></pre> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com0tag:blogger.com,1999:blog-240618461500553130.post-73809588415089769502008-06-24T06:37:00.001+10:002008-06-24T06:37:49.468+10:00Sync for ADO.NET - Client (Part 1)<p>Now it's time to set up our client. I'm not a big fan of wizards, and at this point in time the wizard for Sync is broken for my design so we'll code it all manually. I think this really helps with the understanding of the framework anyway.</p> <p>For the Sync framework the concept is pretty simple, there's a few main roles:</p> <ul> <li>Client Sync Provider - this is what talks to our local SQL CE database </li> <li>Server Sync Provider - this talks to the server SQL database </li> <li>Sync Agent - this contains the group(s) of tables, instances of the providers, and exposes the Synchronize() method</li> </ul> <p>If you're planning on using this in a distributed app where some clients don't have direct access to the SQL server you'll want to host the Server Sync Provider inside a WCF service, this provides for a secure way to talk to the database without exposing it to the Internet.</p> <p>It's also worth noting that if you host using a WCF service layer, the server data store could be anything as long as it supports tracking changes using an anchor.</p> <h2>Sync Agent</h2> <p>This is where we create our groups of tables, each group will be updated at the server using a single transaction, if part of it fails the transaction fails (and will be retried on next sync).</p> <p>For our purposes we'll have a single group with all our tables, we just have to make sure we add the tables in the right order so as not to break a relationship (for this demo it means TimeEntries is added last).</p> <p>We can also add handlers to the StateChanged and SessionProgress events on this class if we want to show some feedback in our UI.</p> <h2>Client Sync Provider</h2> <p>To configure the local SQL CE database the out-of-the-box solution is good enough, but I like to make some minor changes like setting my primary key Guids to have RowGuid = true, as enforce foreign keys (these are dropped by Sync, probably because it's pretty easy to bring over partial results that might not include everything needed for relationships).</p> <p>Again we use event handlers to add extra functionality by subscribing to CreatingSchema and SchemaCreated. There some events handy for logging too SchemaCreated, ChangesSelected, ChangesApplied, ApplyChangeFailed.</p> <p>The above mentioned changes to Primary Key and Foreign Key references would be done in a CreatingSchema event handler, and any ALTER TABLE statements can be done in the SchemaCreated handler.</p> <h2>Server Sync Provider</h2> <p>We need to specify our SelectNewAnchorCommand, which can be as simple as getting the "Min_Active_RowVersion() - 1" (SQL 2005 SP2 fix for @@DBTS issue), or calling a stored proc that gets an anchor with support for batching.</p> <p>For each table we want to synchronize against there's a bunch of SQL statements needed. If you want to go the easy way you can use the SqlSyncAdapterBuilder, just give it a little info about your table and it'll go off and read the schema and create the commands needed. I would suggest doing it by hand the first couple of times so you know exactly what's going on. For enterprise apps, make your DBA's happy and let them create stored procedures for the insert/update/delete pairs.</p> <p>There's some events you can hook into here too, notably ChangesSelected, ChangesApplied, ApplyChangeFailed and ApplyingChanges. Mostly these would be for logging purposes, although in your ApplyingChanges handler you could modify the data about to be written.</p> <p>One important note here, this class would normally exist inside a WCF layer, so we'll put it in another Class Library project for now that way it's easier to drop the reference in our client and use a wrapper around the WCF service at a later date.</p> <h2>What Next?</h2> <p>I'll go into a bit more detail showing some code for the client, perhaps even a download for the solution so far.</p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com1tag:blogger.com,1999:blog-240618461500553130.post-85672812124803878402008-06-22T23:21:00.001+10:002008-06-22T23:21:03.859+10:00Sync for ADO.NET - Server<p>So we have the basic idea of the application, and due to the limited number of tables our Domain Design matches Data Design quite nicely. This means that for simplicity we can use LINQ to SQL, but I'll discuss that more in a post about the client.</p> <p>For the server all we really need to do is create our database for now. It's worth noting that to support sync we'll need to add a few extra columns to each table. It's also important to decide how a <strong>delete</strong> will be handled, either as a logical delete (row is 'flagged' somehow to mark it as deleted) or a physical delete (row is removed from the database). If a physical delete is going to be used you'll need to add a "tombstone" table, somewhere to copy the row to when you delete it, otherwise there's no way for the clients to know the row has been removed (since they'll just ask for changes since last check).</p> <p>Extra columns I suggest are:</p> <ul> <li>UpdateVersion timestamp NOT NULL </li> <li>CreateVersion binary(8) NOT NULL DEFAULT @@DBTS + 1 </li> <li>DeleteVersion varbinary(8) NULL </li> <li>UpdateID uniqueidentifier NOT NULL </li> <li>CreateID uniqueidentifier NOT NULL </li> <li>DeleteID uniqueidentifier NULL </li> </ul> <p>I've made an assumption here that only sync-aware applications will modify rows, this avoids the need for triggers and empty Guid values as defaults. Please note that we'll be filtering rows on all 6 columns so they should all have an index added.</p> <p>Here's my database diagram:</p> <p><a href="http://lh3.ggpht.com/tim.walters/SF5Ru-3td_I/AAAAAAAAABg/CcnqmzpQp5w/TimeTrackerDatabaseDiagram2.png"><img height="117" alt="TimeTracker Database Diagram" src="http://lh6.ggpht.com/tim.walters/SF5RvmjlIbI/AAAAAAAAABk/_WgZ_Em8h6E/TimeTrackerDatabaseDiagram_thumb.png" width="149" /></a> </p> <p>You'll notice that I haven't included the DeleteVersion or DeleteID columns for the 3 related tables, since I don't want to allow deletes on them (later we could add an IsActive flag to allow marking them as inactive while not breaking existing links).</p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com0tag:blogger.com,1999:blog-240618461500553130.post-41287673612536940312008-06-22T22:29:00.001+10:002008-06-22T22:29:24.119+10:00Sync for ADO.NET - Introduction<p>This is the first in a series of posts I plan on my experience with the <a href="http://msdn.microsoft.com/sync/">Microsoft Sync Framework for ADO.NET</a>.</p> <p>First a little about the scenario for this, I'm creating a small app that must run on multiple machines, all talking to a central database (either via LAN or The Internet), supporting full off-line operation.</p> <p>To make this a little more interesting I'm going to assume there's a mix of download-only tables (reference data) and full bi-directional sync tables (the stuff we'll be changing).</p> <p>Clients will be running SQL CE 3.5 SP1, since that's the only choice we have (for now) with the Sync framework. Server will be running SQL 2005 SP2 (SP2 is required to fix an issue with @@DBTS, without it we could have missing data on clients). SQL 2008 would make it easier but it's still rare to find that out in the wild, and it's no big deal to change this to support that later.</p> <p>For my test application it'll be a simple Time-sheet, here's the class diagram:</p> <p><a href="http://lh3.ggpht.com/tim.walters/SF5FoNc4FII/AAAAAAAAABU/yuvu2U1mDwk/Timesheet_ClassDiagram%5B7%5D.png"><img height="117" alt="Timesheet_ClassDiagram" src="http://lh5.ggpht.com/tim.walters/SF5Fozd7NaI/AAAAAAAAABc/QvjyYpL0y5o/Timesheet_ClassDiagram_thumb%5B5%5D.png" width="248" /></a></p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com0tag:blogger.com,1999:blog-240618461500553130.post-40269796536530049912008-06-22T02:03:00.001+10:002008-06-22T02:03:49.036+10:00LINQ to SQL frustration<p>I've been experimenting quite a bit with LINQ to SQL, in combination with the Microsoft Sync Framework for ADO.NET. This places a limitation of SQL CE 3.5 SP1 on the client side, so no stored procs and a sub-set of field types only.</p> <h2>GUID as Primary Key</h2> <p>One thing I noticed is that you can't set a SQL CE table to use a GUID as the Primary Key using NewID() for the default. Sure SQL CE will let you do it, but LINQ to SQL has a fit when you try and insert a row, apparently you're not allowed to mark a 'uniqueidentifier' column as auto-generated... perhaps that's some hangover from the Beta or something, since Sync has some issues too (if you sync a server-side table to a local table it loses the default and loses the "is row guid" property, requiring you to intercept the schema creation to turn those features back on).</p> <p>This is no big deal I guess, since if you are using SQL CE then your app is all that's going to be changing rows, so you can simply add a partial class that sets the GUID column to Guid.NewGuid() in the OnCreated() partial method.</p> <h2>No Designer Support for SQL CE</h2> <p>While you can use SQLMetal.exe to create your mapping, it's not the same experience. Then again, given the nature of my focus here it's probably a better idea since I want to adjust my schema often based on my ever-changing sync requirements.</p> <h2>It's Not a First Class OR/M</h2> <p>This one might get me some hate mail, but I feel that it's a pretty poor OR/M experience. For production code I would suggest NHibernate as it allows for the vast differences between your Domain Model and your Data Model.</p> <p>While LINQ to SQL has some support for inheritance it's still a baby in the OR/M world. Any project beyond 5 tables in size would probably have to be adjusted to fit the technology instead of having the technology make the project easier.</p> <h2>Summary</h2> <p>From my experience so far, I think that LINQ to SQL is great for doing a demo, great for small projects and perhaps some web apps, but can't be taken seriously in the enterprise application development area.</p> <p>Still, that wont stop me from using it as a replaceable data persistance layer for demo apps and in articles I plan to write.</p> Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com0tag:blogger.com,1999:blog-240618461500553130.post-78126596394229245002008-06-06T10:48:00.001+10:002008-06-22T00:48:55.850+10:00Blogging again!Ahh, it's good to be blogging again. I have been quiet for so long I couldn't remember the password for my old blog, nor the email I used to register it... so I've started another one here.<br /><br />I've recently started a 6 month contract fixing a WPF app that has been using CAB, it's own custom Sync Framework, with a SQL 2005 backend... it's a bit of a mess so I'll be looking at changing the architecture. If Sync is needed, I'll be using the Microsoft Syncronization Framework (currently CTP2). I'm also planning to use NHibernate and Castle Active Record to simplify the DAL and get rid of the custom code-generation that's being done right now.<br /><br />I plan to blog about this, and also my experiments (and frustration with) LINQ 2 SQL, WPF Data Binding, and other architecture I play with.<br /><br />If I get any spare time I want to try out the iPhone SDK, since there's a high chance the iPhone will be released here in Australia next week!<br /><br />Anyway, I'll post more soon.Anonymoushttp://www.blogger.com/profile/03935236232529345484noreply@blogger.com1