tag:blogger.com,1999:blog-41710684207340922672024-03-07T21:35:31.192-07:00Digital CamelEverything you'd expect from a hi-tech, 21st century, digital camel.rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.comBlogger16125tag:blogger.com,1999:blog-4171068420734092267.post-81251915977295190292024-01-29T12:05:00.000-07:002024-01-29T12:05:32.984-07:00Tabbed Sub-Reports with SRS and SharePoint<p>Okay... So the title isn't quite as colorful as the <a href="http://digitalcamel.blogspot.com/2008/02/database-dependent-duo-delivers.html" target="_blank">first version</a>, but we'll make up for that with the content. Consider this a more better, revised, updated, newly interpreted, adjusted, adapted, renovated for your future enjoyment, revisit of the previous post. This time, I've got lots of pictures.</p>
<p align="center"/>
<div class="separator" style="clear: both;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhnvkLstV0Iv5NUK6ePpSt5jbCrNhPCBeHhhR0Nd6irmgISHI26t0fvhh4uSBN_vgnpIqDPGt647K_GVWFZwA_qVv-7TdqL7rRfge61DsVBPXY7PnAWZtRzRv0gSLDMXixf2gL-JoOpYSHzFGO9QFNinKjQJZaxeX_4ScQPnu9D2HKqYb35I8_ZjXiPFLA/s1600/SRSTabs3.jpg" style="display: block; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="867" data-original-width="621" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhnvkLstV0Iv5NUK6ePpSt5jbCrNhPCBeHhhR0Nd6irmgISHI26t0fvhh4uSBN_vgnpIqDPGt647K_GVWFZwA_qVv-7TdqL7rRfge61DsVBPXY7PnAWZtRzRv0gSLDMXixf2gL-JoOpYSHzFGO9QFNinKjQJZaxeX_4ScQPnu9D2HKqYb35I8_ZjXiPFLA/w143-h200/SRSTabs3.jpg" width="143"/>
</a>
</div>
<p/>
<p>So here's the skinny: <a href="http://digitalcamel.blogspot.com/2008/02/database-dependent-duo-delivers.html" target="_blank">Previously</a> I posted on using SRS and SharePoint (that's the database dependent duo, just in case nobody got that) to create dynamic sub-reports. Here, I've got a solution that was inspired by my first go-round with this technique. Except this time I'm using a tabbed interface to present several related sub-reports.</p>
<p>If that was the skinny, I suppose this is the fat: Quick and clean - here's how to do it...</p>
<p>Assumptions:</p>
<ul>
<li>You are somewhat familiar with SRS and SharePoint - this isn't a tutorial on the basics, there's plenty of that out there. </li>
<li>You have a system with access to SRS and SharePoint. </li>
<li>SRS is installed in SharePoint integration mode. </li>
<li>You will be using Visual Studio. (Not really <em>required</em>, but that's what my screen shots are of...) </li>
<li>You are somewhat familiar with HTML and Cascading Style Sheets (CSS). </li>
<li>You know what JavaScript is and, at the very least, seeing it doesn't make you curl up into a fetal position in the corner of the room.</li>
</ul>
<p>The example depends on a silly little database I created just to have a simple data schema for my purpose. It's a Family Details database - four tables of completely useless information:</p>
<p align="center"/>
<div class="separator" style="clear: both;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiuG292wlelDgqCyPtfcAUkAT1m61dEoVQNFlG1QrziXDVldhSTy8aO7S8zKED_Q50JRysIUt7RcTbD8X8rgavH7d3a90x2hBo9z4CT5Ko4_TFa3-cMRb1Ji7-p-jhnfEBvOrAQcTevYRHIcOfIdTRZpe28fGUVizKnrorlBcfoKe7vo7Nm0YoD1BTcWyw/s604/SRSTabs12.jpg" style="display: block; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="223" data-original-width="604" height="74" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiuG292wlelDgqCyPtfcAUkAT1m61dEoVQNFlG1QrziXDVldhSTy8aO7S8zKED_Q50JRysIUt7RcTbD8X8rgavH7d3a90x2hBo9z4CT5Ko4_TFa3-cMRb1Ji7-p-jhnfEBvOrAQcTevYRHIcOfIdTRZpe28fGUVizKnrorlBcfoKe7vo7Nm0YoD1BTcWyw/w200-h74/SRSTabs12.jpg" width="200"/>
</a>
</div>
<p align="left">We're working with a collection of 5 very simple reports. One to act as a search utility and one each to report on the data in each table. The first screen shot shows you the full layout. The search report accepts a single parameter that's turned into a search term in the data source query:</p>
<div class="separator" style="clear: both;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZqpda1rumhF07AwVy_NxLCXe4XCmzub_zFEdoOzxIofNqCd1SUcbJwyt8LNMPO3ohvnIKLnPS-qIetCJnXMDc_L3BMeACC1VyLDvwomkFELJ7U6ea4winnd36Q5OGShJOwWinQLjwmCeo65d0I3eimtVHx4EQZ4SdSUm03LMNrjNwejk8yzg8XT3uwTs/s852/SRSTabs7.jpg" style="display: block; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="723" data-original-width="852" height="170" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZqpda1rumhF07AwVy_NxLCXe4XCmzub_zFEdoOzxIofNqCd1SUcbJwyt8LNMPO3ohvnIKLnPS-qIetCJnXMDc_L3BMeACC1VyLDvwomkFELJ7U6ea4winnd36Q5OGShJOwWinQLjwmCeo65d0I3eimtVHx4EQZ4SdSUm03LMNrjNwejk8yzg8XT3uwTs/w200-h170/SRSTabs7.jpg" width="200"/>
</a>
</div>
<p align="left">The basic layout for the report is as simple as could be, but there are two things worthy of note: First, we have to format the field display to look like a hyperlink, since SRS doesn't do that for us:</p>
<div class="separator" style="clear: both;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3Q9FCS1ZoEKPbOfdGj8bXugmcqxTZvmrDxUupuwKHZ54shuguuSdl7Xi_s-vNTPwkDERWW4Yqyx6B7xt8XN58OeWyh5vNYJSQChVh5r1jpWh7e4jwSlDWBEIONQPcQD684gRgIz8QeSmGo6tA9QUR3G8vp4qwEwfzgAY_RvwLiek4_pXtpYyyBxGf2lM/s832/SRSTabs9.jpg" style="display: block; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="596" data-original-width="832" height="143" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3Q9FCS1ZoEKPbOfdGj8bXugmcqxTZvmrDxUupuwKHZ54shuguuSdl7Xi_s-vNTPwkDERWW4Yqyx6B7xt8XN58OeWyh5vNYJSQChVh5r1jpWh7e4jwSlDWBEIONQPcQD684gRgIz8QeSmGo6tA9QUR3G8vp4qwEwfzgAY_RvwLiek4_pXtpYyyBxGf2lM/w200-h143/SRSTabs9.jpg" width="200"/>
</a>
</div>
<p align="left">
Second, we need to configure the field to call our JavaScript function. We'll see the script in a bit, but here's where we configure the call:
</p>
<div class="separator" style="clear: both;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirk5hTY9dSz6XCImltg0eOLDvaLT1oMHXHasqfYBhTMrNDFdnmu1tGrczAD-ORo_JnvCT9zmeK79aXmQCwCjqHi0s2oZVLfm4E_OZeNpwnZpc40AhH_cHeU8t_Yzxa-6BC107FEGxNcjFqqbnCCPQnsTHWfuM98xC2CikTN7RCDxmyAF2F6HKUwgUd8T0/s832/SRSTabs6.jpg" style="display: block; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="596" data-original-width="832" height="143" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirk5hTY9dSz6XCImltg0eOLDvaLT1oMHXHasqfYBhTMrNDFdnmu1tGrczAD-ORo_JnvCT9zmeK79aXmQCwCjqHi0s2oZVLfm4E_OZeNpwnZpc40AhH_cHeU8t_Yzxa-6BC107FEGxNcjFqqbnCCPQnsTHWfuM98xC2CikTN7RCDxmyAF2F6HKUwgUd8T0/w200-h143/SRSTabs6.jpg" width="200"/>
</a>
</div>
<p align="left">In this implementation, that's the only place we need to configure anything out of the ordinary in the report. The rest of the reports are all very similar. Each accepts a single parameter indicating the ID of the family to report on. They are not connected in any way within the RDL file or SRS. All of the connecting magic takes place in our script. Here's a look at the FamilyDetails report:</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGH_8hcfIK96gjjPEIg-kVGTjc56yo9vLZZyiZkDbxrr-m8gRMA3Ln8HmljE9T4XWBlAIolxxyMei9tn6OWbzLyHDllMxy8w1altFpv0_pSL9-2EUzDMTeYPC0xH1SOb7D9gWXMqLGhi3-TAkI0r4B2Qa1GMdyFR_oSL2xPQkJmOVIkIPSLJQqR2CCciA/s580/SRSTabs8.jpg" style="display: inline; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="480" data-original-width="580" height="166" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGH_8hcfIK96gjjPEIg-kVGTjc56yo9vLZZyiZkDbxrr-m8gRMA3Ln8HmljE9T4XWBlAIolxxyMei9tn6OWbzLyHDllMxy8w1altFpv0_pSL9-2EUzDMTeYPC0xH1SOb7D9gWXMqLGhi3-TAkI0r4B2Qa1GMdyFR_oSL2xPQkJmOVIkIPSLJQqR2CCciA/w200-h166/SRSTabs8.jpg" width="200" />
</a>
<span style="text-align: left;"> </span>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEibYT8f3JmvgBMIWxhQE5H8Bx6ZFpWCd6wyme6UyDtRCW6C2l6bDjhRNbVTMVhCscixO3Xd6uDQDtfwEhppeQT4kOf0dZVYG21b_afEoOws-ppA1jA3ITsvfvEg_0Oi4DO8mminH_AeuH4wrQ1Aj-SwDqgz9Ml-76HsUoMiIG5QRt7lyfCUa_YgcUYipXU/s832/SRSTabs10.jpg" style="display: inline; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="596" data-original-width="832" height="143" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEibYT8f3JmvgBMIWxhQE5H8Bx6ZFpWCd6wyme6UyDtRCW6C2l6bDjhRNbVTMVhCscixO3Xd6uDQDtfwEhppeQT4kOf0dZVYG21b_afEoOws-ppA1jA3ITsvfvEg_0Oi4DO8mminH_AeuH4wrQ1Aj-SwDqgz9Ml-76HsUoMiIG5QRt7lyfCUa_YgcUYipXU/w200-h143/SRSTabs10.jpg" width="200" />
</a>
</div>
<p align="left">Next we move on to SharePoint (WSS required - MOSS optional). All of the reports are deployed to the WSS document library configured in the SRS SharePoint integration setup. For organizational purposes, I created another document library, called <em>Dashboards</em>, to hold the web part page that will become the <em>Family</em>
<em>Information</em> dashboard. I chose the basic <em>Full Page, Vertical </em>web part page template. I only need two web parts for this implementation. The first is a SQL Server Reporting Services Report Viewer web part.</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhLdSJdIqU7dTqE2mOSGMCxwyF_pATshLOOuQHhZN_huteIJw5QE5dJGovgGP3uKEZvR-IIKw3SZsSuPbVdWzSiXwc0Ah2hW8ibKhOsT-Vdn8NGFMN-j3hCEP1S5M9uhlUxZkWa3sEcd0Zhkxa8sEAV1XY8jK1FAJgVPyLvAZ5U4Xn7lxlhLO3jtUfyyCY/s596/image.png" style="margin-left: 1em; margin-right: 1em;">
<img border="0" data-original-height="147" data-original-width="596" height="79" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhLdSJdIqU7dTqE2mOSGMCxwyF_pATshLOOuQHhZN_huteIJw5QE5dJGovgGP3uKEZvR-IIKw3SZsSuPbVdWzSiXwc0Ah2hW8ibKhOsT-Vdn8NGFMN-j3hCEP1S5M9uhlUxZkWa3sEcd0Zhkxa8sEAV1XY8jK1FAJgVPyLvAZ5U4Xn7lxlhLO3jtUfyyCY/s320/image.png" width="320"/>
</a>
</div>
<p align="left">It's configured to display the search report:</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhC0mCWuphN4at3shH_3sigUwFcXUHHPKEI8u-6VIDFiy_85B3DOt6MYoP72qsjV3U1iDFfAczAebz259AYpnp-Fi1Nyov_HAJY3aqUC1i3tXX-OrHXXmJoNvwkGAStN7f_vVKv_JDiSb81FD6fhEgrI1GjTjkeQCFaX2OhnSrtbG9zJhOh8u1xS7YIQG0/s867/SRSTabs1.jpg" style="margin-left: 1em; margin-right: 1em;">
<img border="0" data-original-height="867" data-original-width="621" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhC0mCWuphN4at3shH_3sigUwFcXUHHPKEI8u-6VIDFiy_85B3DOt6MYoP72qsjV3U1iDFfAczAebz259AYpnp-Fi1Nyov_HAJY3aqUC1i3tXX-OrHXXmJoNvwkGAStN7f_vVKv_JDiSb81FD6fhEgrI1GjTjkeQCFaX2OhnSrtbG9zJhOh8u1xS7YIQG0/w143-h200/SRSTabs1.jpg" width="143"/>
</a>
</div>
<p align="left">A user enters some value in the parameter box and clicks Apply. This is where we get the report configured with the navigation links (the ones that call our JavaScript - yes... It's coming...)</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3iXf3KWjxy1816p7oahy6vtaJgWNzNn-Mc5YKN7vS7fzDxO3427mSulelRKFi_ER-ruFcsAkylxy5JBAGO6B7fiGfXGhcmxY4E0w0iD6Woyjc023e6OMGquxfqXfeyLp-7GJUmV0ZHRPeUhR4PkUQNtp3xmXRUenQTGos80CCPY6mv9MpIIndmlRptUQ/s867/SRSTabs2.jpg" style="margin-left: 1em; margin-right: 1em;">
<img border="0" data-original-height="867" data-original-width="621" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3iXf3KWjxy1816p7oahy6vtaJgWNzNn-Mc5YKN7vS7fzDxO3427mSulelRKFi_ER-ruFcsAkylxy5JBAGO6B7fiGfXGhcmxY4E0w0iD6Woyjc023e6OMGquxfqXfeyLp-7GJUmV0ZHRPeUhR4PkUQNtp3xmXRUenQTGos80CCPY6mv9MpIIndmlRptUQ/w143-h200/SRSTabs2.jpg" width="143"/>
</a>
</div>
<p align="left">The other web part is a Content Editor Web Part. This is where the JavaScript lives. But before we go there, there's one more bit we need to make this complete. The tabs themselves use images as a background.</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3TQ31jUOspANjzL-x1gMPH0147Enhe2CEwjGwCCdPeIFrv191NB89AiQijPoViAEB3DoOVNtBnFWPRkNblYniZ1qj18hUvoZ_sXeY4lfIWKc7dYjq_QJmWp4t6heMNDaXHuUYhnyxQ5b7-3d1DNLTCUEBD7K-wUmlra3APtzRMEUEfGuVgdBZ5DIG9lU/s100/tabhot100.jpg" style="display: inline; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="25" data-original-width="100" height="25" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3TQ31jUOspANjzL-x1gMPH0147Enhe2CEwjGwCCdPeIFrv191NB89AiQijPoViAEB3DoOVNtBnFWPRkNblYniZ1qj18hUvoZ_sXeY4lfIWKc7dYjq_QJmWp4t6heMNDaXHuUYhnyxQ5b7-3d1DNLTCUEBD7K-wUmlra3APtzRMEUEfGuVgdBZ5DIG9lU/s1600/tabhot100.jpg" width="100"/>
</a>
<span style="text-align: left;"> </span>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh8WDDKAccLgfDhr7HMehi7Mmqbmgyf887-EGRbYvyHjbFhiqyXFy9sToRtoCRT-FxiSSWf7ryGAMVoUIIqHQu4vqx9R-qkKyOAjGRIT2JnUPOULlTMBufSormF7G8rVNdA4OguDTm0J_B8QKL6uL8WygO_CKvUAtHA1EAa678XMCCJ3FlYF9npFRxAwpU/s100/tab100.jpg" style="display: inline; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="25" data-original-width="100" height="25" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh8WDDKAccLgfDhr7HMehi7Mmqbmgyf887-EGRbYvyHjbFhiqyXFy9sToRtoCRT-FxiSSWf7ryGAMVoUIIqHQu4vqx9R-qkKyOAjGRIT2JnUPOULlTMBufSormF7G8rVNdA4OguDTm0J_B8QKL6uL8WygO_CKvUAtHA1EAa678XMCCJ3FlYF9npFRxAwpU/s1600/tab100.jpg" width="100"/>
</a>
</div>
<br/>
<p/>
<p align="left">One image for the selected (or hot) tab and another image for the unselected tabs. To make the images easily accessible to the web part, I dropped them here:</p>
<p align="center">
<em>C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\IMAGES\Ensynch</em>
</p>
<p align="left">In WSS, that TEMPLATES folder is accessible through the _layouts virtual directory in any site.</p>
<p align="left">Okay, so this other web part... Place a Content Editor Web Part under the Report Viewer web part:</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmJHr8aihYMl7JpqtZcEFXoqxF9r08p_pajxi8xS3puM0UGUDa1BIRlH3PGkRZ0o3ASL6HEtA8dDBaMTQMEwV5bigeFg1UXKwWCJGtiPbwSy_siIARgAB9gMDvdb8yve-DAlWmDJcKXYYFAZRLUy2-DYmbNfQKvvrSbUJr5iT5chy6yNl4skwgMzD1FwI/s824/SRSTabs11.jpg" style="margin-left: 1em; margin-right: 1em;">
<img border="0" data-original-height="777" data-original-width="824" height="189" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmJHr8aihYMl7JpqtZcEFXoqxF9r08p_pajxi8xS3puM0UGUDa1BIRlH3PGkRZ0o3ASL6HEtA8dDBaMTQMEwV5bigeFg1UXKwWCJGtiPbwSy_siIARgAB9gMDvdb8yve-DAlWmDJcKXYYFAZRLUy2-DYmbNfQKvvrSbUJr5iT5chy6yNl4skwgMzD1FwI/w200-h189/SRSTabs11.jpg" width="200"/>
</a>
</div>
<p align="left">
<span style="text-align: left;">For clarity, I've broken out the code into sections, here, but it's all in the one web part. Make sure that when you go to add the code that you click the </span>
<em style="text-align: left;">Source Editor</em>
<span style="text-align: left;"> button, not the </span>
<em style="text-align: left;">Rich Text Editor</em>
<span style="text-align: left;">.</span>
</p>
<p align="left">The code consists of a bit of style sheet information:</p>
<div style="background-color: #80c0ff; color: white; font-family: courier; font-size: 8px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;"><style> Element:</div>
<div style="background-color: white; color: black; font-family: courier; font-size: 8px; line-height: 20px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;"><style><br/> .ensTab100 { height:25px;<br/> width:100px;<br/> vertical-align: middle;<br/> text-align: center;<br/> background-image : ur(/_layouts/images/ensynch/tab100.jpg);<br/> color: #ffffff;<br/> font-family: Arial;<br/> font-size: 12px;<br/> font-weight: bold;<br/> cursor: pointer; }<br/>
<br/> .ensTabHot100 { height:25px;<br/> width: 100px;<br/> vertical-align: middle;<br/> text-align: center;<br/> background-image : url(/_layouts/images/ensynch/tabhot100.jpg);<br/> color: #000000;<br/> font-family: Arial;<br/> font-size: 12px;<br/> font-weight: bold;<br/> cursor: pointer; }<br/></style></div>
<p align="left">A wrapper <div> that allows us to hide the stuff that has no content until we're ready for it: (This is strictly for aesthetics.)</p>
<div style="background-color: #80c0ff; color: white; font-family: courier; font-size: 8px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;">Wrapper <div>:</div>
<div style="background-color: white; color: black; font-family: courier; font-size: 8px; line-height: 20px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;"><div id="ReportWrapper" style="visibility: hidden;"></div>
<p align="left">An HTML iframe for the main family report: (Note that the <em>src</em> attribute starts out empty and the <em>id</em> attribute is required.)</p>
<div style="background-color: #80c0ff; color: white; font-family: courier; font-size: 8px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;">Main Report <iframe>:</div>
<div style="background-color: white; color: black; font-family: courier; font-size: 8px; line-height: 20px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;"><div style="height: 100px; width: 100%"><br/> <iframe id="FamilyMain" style="border-style: none; height: 100%; width: 100%" src=""></iframe><br/></div></div>
<p align="left">A table structure to hold the tabs: (Note the <em>id</em>, <em>class</em> and <em>onclick</em> event settings - all required.)</p>
<div style="background-color: #80c0ff; color: white; font-family: courier; font-size: 8px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;">Tab <table>:</div>
<div style="background-color: white; color: black; font-family: courier; font-size: 8px; line-height: 20px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;"><table cellpadding="0" cellspacing="1"><br/> <tr><br/> <td id="tab1" class="ensTabHot100" onclick="SetCurrentTab(this.id);">Friends</td><br/> <td id="tab2" class="ensTab100" onclick="SetCurrentTab(this.id);">Food</td><br/> <td id="tab3" class="ensTab100" onclick="SetCurrentTab(this.id);">Colors</td><br/> </tr><br/></table></div>
<p align="left"> An HTML iframe for the sub-report of the active tab: (Note that the <em>src</em> attribute starts out empty and the <em>id</em> attribute is required.)</p>
<div style="background-color: #80c0ff; color: white; font-family: courier; font-size: 8px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;">Main Report <iframe>:</div>
<div style="background-color: white; color: black; font-family: courier; font-size: 8px; line-height: 20px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;"><div style="height: 100px; width: 100%"><br/> <iframe id="FamilySub" style="border-style: none; height: 100%; width: 100%" src=""></iframe><br/></div></div>
<p align="left"> Close the wrapper <div>:</p>
<div style="background-color: #80c0ff; color: white; font-family: courier; font-size: 8px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;">Wrapper </div>:</div>
<div style="background-color: white; color: black; font-family: courier; font-size: 8px; line-height: 20px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;"></div></div>
<p align="left"> And, of course, the script that connects it all:</p>
<div style="background-color: #80c0ff; color: white; font-family: courier; font-size: 8px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;"><script> Element:</div>
<div style="background-color: white; color: black; font-family: courier; font-size: 8px; line-height: 20px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; padding: 5px;"><script type="text/javascript"><br/> var currentTab = "tab1";<br/> var currentID = "";<br/>
<br/> function SetFamilyID(famID)<br/> {<br/> if (currentID != famID)<br/> {<br/> currentID = famID;<br/> UpdateMain();<br/> }<br/> }<br/>
<br/> function SetCurrentTab(tabName)<br/> {<br/> if (currentTab != tabName)<br/> {<br/> document.getElementById(currentTab).className = "ensTab100";<br/> document.getElementById(tabName).className = "ensTabHot100";<br/> currentTab = tabName;<br/> UpdateTabs();<br/> }<br/> }<br/>
<br/> function UpdateMain()<br/> {<br/> if (currentID != "")<br/> {<br/> document.getElementById('ReportWrapper').style.visibility = "visible";<br/> document.getElementById('FamilyMain').src=<br/> 'http://win2k3/reportserver?' +<br/> 'http://win2k3/ilm/reports/FamilyDetails.rdl&' +<br/> 'rs:Command=Render&rc:Toolbar=false&rc:Parameters=false&' +<br/> 'FamilyID=' + currentID; <br/> UpdateTabs();<br/> }<br/> }<br/>
<br/> function UpdateTabs()<br/> {<br/> var report = "";<br/> if (currentID != "")<br/> {<br/> switch(currentTab)<br/> {<br/> case "tab1":<br/> report = "FamilyFriends.rdl"<br/> break;<br/> case "tab2":<br/> report = "FamilyFood.rdl"<br/> break;<br/> case "tab3":<br/> report = "FamilyColors.rdl"<br/> break;<br/> }<br/>
<br/> if (report != "")<br/> {<br/> document.getElementById('FamilySub').src=<br/> 'http://win2k3/reportserver?' +<br/> 'http://win2k3/ilm/reports/' + report + '&' +<br/> 'rs:Command=Render&rc:Toolbar=false&rc:Parameters=false&' +<br/> 'FamilyID=' + currentID;<br/> }<br/> }<br/> }<br/></script></div>
<p align="left"> Here's a basic rundown of how the plumbing works:</p>
<ul>
<li>
<div align="left">The script sets up a couple of global variables:</div>
</li>
<ul>
<li>
<div align="left">
<em>currentTab</em> - hold the id of the current tab. Defaults to "tab1".</div>
</li>
<li>
<div align="left">
<em>currentID</em> - holds the ID of the family that was selected in the search report.</div>
</li>
</ul>
<li>
<div align="left">A user clicks on a link presented by the search report, calling SetFamilyID() and passing in the ID.</div>
</li>
<li>
<div align="left">SetFamilyID stores that value in a global variable (<em>currentID</em>) and then calls UpdateMain().</div>
</li>
<li>
<div align="left">UpdateMain, assuming that there is a value in <em>currentID:</em>
</div>
</li>
<ul>
<li>
<div align="left">Makes sure that the wrapper <div> is visible</div>
</li>
<li>
<div align="left">Sets the proper <em>src</em> value for the <em>FamilyMain</em> iframe (including the ID parameter).</div>
</li>
<li>
<div align="left">Calls UpdateTabs();</div>
</li>
</ul>
<li>
<div align="left">UpdateTabs() then proceeds to:</div>
</li>
<ul>
<li>
<div align="left">Determine which report to run based on the value in <em>currentTab</em>.</div>
</li>
<li>
<div align="left">Set the proper <em>src</em> value for the <em>FamilySub</em> iframe (including the ID parameter).</div>
</li>
</ul>
<li>
<div align="left">When a user click on one of the tabs, a call is made to SetCurrentTab, passing in the id of the tab that was clicked on.</div>
</li>
<li>
<div align="left">SetCurrentTab(), assuming that the clicked tab is not already the current tab:</div>
</li>
<ul>
<li>
<div align="left">Sets the current tab class to <em>ensTab100</em> so it's no longer "hot".</div>
</li>
<li>
<div align="left">Sets the new tab class to <em>ensTabHot100</em> so it has the selected appearance.</div>
</li>
<li>
<div align="left">Saves the new tab value into <em>currentTab</em>.</div>
</li>
<li>
<div align="left">Calls UpdateTabs() to get the new report displayed.</div>
</li>
</ul>
<li>
<div align="left">When a user clicks a new value in the search report (yes, it's still there) - it all starts over again.</div>
</li>
</ul>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEig2zOwrOf5sJcK5V35A6DYdZ9hzxWRHhRgqNOU8tGL8NM2cGePuTW1PH1jySEQ5cMc3cqUG2Vy_eDA_UFnrfWOdEPjN_ENlul0GKPYDAFs7On2f-dHoqoO02_isJEOLi4PIM_YYQN_GijNjT20UfgHNb-RPw_aD-XKDqMupWV0TfqpcUCHch3lzCh13Eo/s867/SRSTabs3.jpg" style="display: inline; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="867" data-original-width="621" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEig2zOwrOf5sJcK5V35A6DYdZ9hzxWRHhRgqNOU8tGL8NM2cGePuTW1PH1jySEQ5cMc3cqUG2Vy_eDA_UFnrfWOdEPjN_ENlul0GKPYDAFs7On2f-dHoqoO02_isJEOLi4PIM_YYQN_GijNjT20UfgHNb-RPw_aD-XKDqMupWV0TfqpcUCHch3lzCh13Eo/w143-h200/SRSTabs3.jpg" width="143"/>
</a>
<span style="text-align: left;"> </span>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiuB0nQkv-BzvQEvU0lHNcVo1Qe6SbvzwqoOcN04ZM_UAIlcFIlzQOfACrUMMWAODZhLWn6apJoUWRX78se7XUrG9041bKO9wP8FbevDXUs9JMQYJ9lj6YSUYvPNwFIvYva6SOvFDKAccNKc-j4vfdMWjFTGDt3n03g9w9altA9apZqABT3T2Mt78VFC8k/s867/SRSTabs4.jpg" style="display: inline; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="867" data-original-width="621" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiuB0nQkv-BzvQEvU0lHNcVo1Qe6SbvzwqoOcN04ZM_UAIlcFIlzQOfACrUMMWAODZhLWn6apJoUWRX78se7XUrG9041bKO9wP8FbevDXUs9JMQYJ9lj6YSUYvPNwFIvYva6SOvFDKAccNKc-j4vfdMWjFTGDt3n03g9w9altA9apZqABT3T2Mt78VFC8k/w143-h200/SRSTabs4.jpg" width="143"/>
</a>
<span style="text-align: left;"> </span>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEisltXD36UWAnaegz78pQzXJlUCPNgm7DW1p7FoQkkosMFPjQ5z5aGag-p4B7K-P4UmNQQZAIDmrhIgsGuATMP8BHYPFv4-t1oR3IlrNaiFDjCZBL7SJ_SP7BWSZBnzlO-jX2KeGuDVCP2tAXKFWspPDcjETOuGynvoUDVSLE0dIEMn-i0evzZ1iYWYSVg/s867/SRSTabs5.jpg" style="display: inline; padding: 1em 0px; text-align: center;">
<img border="0" data-original-height="867" data-original-width="621" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEisltXD36UWAnaegz78pQzXJlUCPNgm7DW1p7FoQkkosMFPjQ5z5aGag-p4B7K-P4UmNQQZAIDmrhIgsGuATMP8BHYPFv4-t1oR3IlrNaiFDjCZBL7SJ_SP7BWSZBnzlO-jX2KeGuDVCP2tAXKFWspPDcjETOuGynvoUDVSLE0dIEMn-i0evzZ1iYWYSVg/w143-h200/SRSTabs5.jpg" width="143"/>
</a>
</div>
<p align="left">Use it in health! (Well, use it in SharePoint, but you know what I mean...) </p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com3tag:blogger.com,1999:blog-4171068420734092267.post-47354439805995510652010-08-30T13:08:00.001-07:002010-08-30T13:08:39.688-07:00FIM Best Practices Volume 1 Released<p><a href="http://blog.ilmbestpractices.com/2010/08/book-is-here-fim-best-practices-volume.html"><font color="#ffdfbf">Identity Manager GURUs and Microsoft MVPs David Lundell and Brad Turner have (finally) released the first volume in their FIM Best Practices Series.</font></a></p> <p>Check out David’s blog: <a href="http://blog.ilmbestpractices.com/2010/08/book-is-here-fim-best-practices-volume.html">http://blog.ilmbestpractices.com/2010/08/book-is-here-fim-best-practices-volume.html</a></p> <p>Then buy the book.</p> <p>Now.</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com0tag:blogger.com,1999:blog-4171068420734092267.post-49191824939257833182010-08-20T10:53:00.001-07:002010-08-20T10:53:18.447-07:00Interacting with PowerShell from C#<p>Recently I had a client that wanted to be able to call PowerShell scripts from their Linux based Java application.  To facilitate, I created a web service that accepts a script name and a parameters collection and returns a customized response object.  There’s a helpful post on <a href="http://www.codeproject.com/KB/cs/HowToRunPowerShell.aspx" target="_blank">CodeProject</a> with some sample code for calling PowerShell from C#.  It demonstrates how to instantiate a PowerShell pipeline and call cmdlets.  It even shows how to drop your application’s variables in the Pipeline, but it isn’t quite complete.  I also needed to setup command line arguments and see what changes the script made to my variable.</p> <p>Basing my code on the CodeProject sample (thanks, jpmik) referenced above, I needed to add in some code to insert command line arguments.  That’s pretty easy as the PowerShell Command object has a Parameters collection.  You just have to instantiate the command object, configure it and drop it into the Pipeline.</p> <p>Making an object variable available to the script was pretty easy, too.  The CodeProject example shows the use of the SetVariable method.  But if the PowerShell script changes the object, you have to follow up with a GetVariable call to pull the changed object out of the Pipeline.  Your object comes back wrapped up in a PSObject object, so you’ll have to dig it out.</p> <p>Here’s what it looks like (this is a code fragment, some supporting elements are not included such as the definition of the MyResponse object):</p> <div style="padding-bottom: 5px; background-color: #80c0ff; padding-left: 5px; padding-right: 5px; font-family: courier; color: #ffffff; font-size: 8px; padding-top: 5px">Interacting with PowerShell from C#:</div> <div style="padding-bottom: 5px; line-height: 20px; background-color: #ffffff; padding-left: 5px; padding-right: 5px; font-family: courier; color: #000000; font-size: 8px; padding-top: 5px">runspace = RunspaceFactory.CreateRunspace(); <br />runspace.Open(); <br />runspace.SessionStateProxy.SetVariable("MyResponse", response); <br />Pipeline pipeline = runspace.CreatePipeline(); <br /> <br />Command cmd = new Command(scriptPath); <br />cmd.Parameters.Add(args); <br />pipeline.Commands.Add(cmd); <br /> <br />Collection<PSObject> results = pipeline.Invoke(); <br /> <br />PSObject newResponse = (PSObject)runspace.SessionStateProxy.GetVariable("MyResponse"); <br />response = (MyResponse)newResponse.BaseObject; <br /> <br />runspace.Close(); </div> <p>Thanks for reading.  Good luck.</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com0tag:blogger.com,1999:blog-4171068420734092267.post-39433083859980724862009-12-31T22:00:00.000-07:002009-12-31T22:00:09.718-07:00Foundation Beyond Belief<object width="480" height="295"><param name="movie" value="http://www.youtube.com/v/-8uhWVgJVqs&hl=en_US&fs=1&rel=0"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/-8uhWVgJVqs&hl=en_US&fs=1&rel=0" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="480" height="295"></embed></object> <ul> <li> <p>Foundation Beyond Belief is a non-profit charitable and educational foundation created (1) to focus, encourage and demonstrate the generosity and compassion of atheists and humanists, and (2) to provide a comprehensive education and support program for nontheistic parents.</p> </li> </ul> <ul> <li> <p>The Foundation will feature ten charitable organizations per quarter.</p> </li> </ul> <ul> <li> <p>Members join by signing up for a monthly automatic donation in the amount of their choice, and distribute it however they wish among the categories. Contributions are fully tax-deductible.</p> </li> </ul> <ul> <li> <p>Members can join a social network and forums centered on the ten categories of giving, advocate for causes, and help us choose new beneficiaries each quarter.</p> </li> </ul> <ul> <li> <p>Featured beneficiaries may be founded on any worldview so long as they do not proselytize. At the end of each quarter, 100 percent of the donations are forwarded and a new slate of beneficiaries selected.</p> </li> </ul> <ul> <li> <p>On the educational side, the Foundation will help create and fund local groups for the education and social support of humanist/atheist parents.</p> </li> </ul> <ul> <li> <p><b>Our Mission:</b> To demonstrate humanism at its best by supporting efforts to improve this world and this life; to challenge humanists to embody the highest principles of humanism, including mutual care and responsibility; and to help and encourage humanist parents to raise confident children with open minds and compassionate hearts.</p> </li> </ul> <p><a href="http://foundationbeyondbelief.org" target="_blank">Visit the Foundation Beyond Belief Website</a></p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com0tag:blogger.com,1999:blog-4171068420734092267.post-81708077557487589212008-07-15T14:59:00.001-07:002008-07-15T14:59:19.756-07:00Single Page Server Side Cookie Check (With Almonds)<p>Here's a bit of .Net code that allows you to check the state of a browser's cookie settings... Enabled or Disabled. It does use a redirect, but it still only requires a single page: </p> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td style="color: white; background-color: #80c0ff" valign="top">Sample Default.aspx.cs</td></tr> <tr> <td style="color: black; background-color: white" valign="top"><font face="Courier New" size="2">// default namespace directives here...<br><br>public partial class _Default : System.Web.UI.Page<br>{<br> bool cookies = false;<br><br> protected void Page_Load(object sender, EventArgs e)<br> {<br> cookies = cookiecheck();<br> Response.Write("cookies = " + cookies.ToString());<br> }<br><br> private bool cookiecheck()<br> {<br> if (Session["CookiesEnabled"] != null)<br> return true;<br><br> if (!IsPostBack)<br> {<br> if (Page.Request.QueryString["ce"] == null)<br> {<br> Session["CookiesEnabled"] = true;<br> Response.Redirect(Request.Url.ToString() + "?ce=1");<br> }<br> }<br><br> return false;<br> }<br>}</font></td></tr></tbody></table> <p></p><a href="http://11011.net/software/vspaste"></a>Now go eat some almonds.<a href="http://11011.net/software/vspaste"></a> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com0tag:blogger.com,1999:blog-4171068420734092267.post-11016739596236647042008-07-08T21:53:00.001-07:002008-07-08T23:36:47.679-07:00enSynch Slated to Take Over the World<div align="center"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="75" alt="enSynch, Inc." src="http://www.camelogic.com/digitalcamel/enSynchSlatedtoTakeOvertheWorld_C9C/image.png" width="141" border="0"></div> <p>Based in Tempe, AZ, enSynch, Inc. is one of the best IT service organizations I've ever had the privilege to work with. And now they're getting even better! With their agreement to acquire EBI Solutions, enSynch is further securing its position as the leading IT service organization in the Southwest.</p> <p>You can read all about enSynch <a href="http://www.ensynch.com" target="_blank">here</a> and if you slide your mouse over <a href="http://www.ebisolutions.com/" target="_blank">here</a> you can find all the insight you need on EBI.</p> <p>The full press release is <a href="http://www.forbes.com/businesswire/feeds/businesswire/2008/07/07/businesswire20080707005560r1.html" target="_blank">here</a>. Truly there is no reason to look anywhere else for your Microsoft infrastructure needs. There is a heap of talent in that organization covering Messaging (Unified or otherwise), SharePoint, Identity Management, Business intelligence, Process Automation and pretty much any other buzz-word you can think of.</p> <p>Engage enSynch and success is inevitable.</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com1tag:blogger.com,1999:blog-4171068420734092267.post-17596264489990954552008-05-29T08:57:00.001-07:002020-01-14T14:52:22.351-07:00Hitler's Toothbrush<p>You know how anyone caught wearing a toothbrush style mustache these days would be looked at with a bit of disdain? (Well, from most people...) This latest <a href="https://www.huffpost.com/entry/dunkin-donuts-pulls-ad-fe_n_103859?guccounter=1&guce_referrer=aHR0cHM6Ly93d3cuYmluZy5jb20vc2VhcmNoP3E9bWljaGVsbGUrbWFsa2luK3JhY2hlbCtyYXkrc2NhcnJmJmZvcm09UFJVU0VOJm1rdD1lbi11cyZodHRwc21zbj0xJm1zbmV3cz0xJnJlY19zZWFyY2g9MSZyZWZpZz0wYWRkOTkwNTRhMzE0YTNmODA1MzUxZDY1ZDFmNjgzNyZzcD0tMSZwcT1taWNoZWxsZSttYWxraW4rcmFjaGVsK3JheStzY2FyJnNjPTAtMzEmcXM9biZzaz0mY3ZpZD0wYWRkOTkwNTRhMzE0YTNmODA1MzUxZDY1ZDFmNjgzNw&guce_referrer_sig=AQAAAMCz2wjzemeM_XemJYAhLsNvcMzNB3FBelMg78BgY7mzjYep9PCzrrI7gsnTf9uvj-5rhQDyI2z8KLIyPnnIF2_kZR5GfqM5I0Uw9HHa7DAsMSBnr0KWjiFtUO3mhBONjKmX0ITiw1hAgJa1HxT_S6Vi-OetkFry7kt0upBqghA7" target="_blank">article</a> tells me that we're taking this stuff a bit too far. I mean, really? The iced coffee pitch is a subliminal message purporting the virtues of terrorism? <a href="http://en.wikipedia.org/wiki/Yasser_Arafat" target="_blank">Yasser Arafat</a> is now defining fashion in the U.S.? Michelle Malkin, the author of the terrorism comments, ought to be ashamed of herself. How hard do you have to be looking to see symbolism in Rachael Ray's neck scarf? People are gullible - You say something like that and they believe you. (See... You believed that, didn't you?) I mean... To the point that they actually pulled the ads.</p> <p>What if Hitler had never <a href="http://www.telegraph.co.uk/news/worldnews/1550768/Hitler-was-ordered-to-trim-his-moustache.html" target="_blank">trimmed</a> his mustache? The entire mustache wearing world have been deeply affected. Fortunately, for the world, he was caught wearing a decidedly unattractive caterpillar looking patch of pubic hair that won't soon be missed. Let's be clear... I am <strong>not</strong> saying that caterpillars were in any way responsible for The Holocaust! If a guy dresses like Michael Jackson, it doesn't mean he can dance.</p> <p>We need to grow up. Outside of the fashion industry, people are not defined by their facial hair or their scarves. In the real world (shoot... even in most virtual worlds) people are defined by their actions. What's the real problem, Malkin? Didn't get your caffeine that morning?</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com1tag:blogger.com,1999:blog-4171068420734092267.post-24462833364214005622008-02-12T01:23:00.001-07:002008-02-12T02:09:34.927-07:00Database Dependent Duo Delivers Dazzling Dynamic Data Display<p>How many of you out there - raise your hands - have databases with more than one table? Now, how many of you - raise your hands, again - actually raised your hands? You know I can't actually see you, right? (And in case you answered that out loud... I can't hear you, either.)</p> <p>I do a lot of work with <a href="http://www.microsoft.com/windowsserver2003/technologies/idm/ilm.mspx" target="_blank">Microsoft's Identity Lifecycle Manager</a> and I'm quite privileged to do much of that work with one, <a href="http://www.identitychaos.com" target="_blank">Brad Turner</a> - Identity Management MVP and all around nice guy. (Funny looking, but nice.) Brad has developed a series of SQL Server Reporting Services based <a href="http://www.identitychaos.com/2007/05/update-miis-reporting-pack-announced.html" target="_blank">reports</a> for viewing the status and history of the identity data and management agent processing for your ILM (MIIS) system. Over the course of several implementations, we have used these reports as the foundation for developing an ILM Management Portal... We are co-presenting on this <a href="http://www.directoryexpertsconference.com/agendaandspeakers/abstracts.cfm#designingidentity" target="_blank">topic</a> at the upcoming <a href="http://www.dec2008.com" target="_blank">DEC 2008</a> conference in Chicago.</p> <p>But, this is not the story of our ILM portal. This is the story of a nifty little technique I developed in order to make the data we present in our portal a bit more dynamic. So, the only reason I bring up the history is to brag a bit about our awesome portal solution and our upcoming DEC presentation. The rest of this is best illustrated with a simple example...</p> <p>Let's go stereotypes... You're a business. You have data. You have data about your products. You have data about your customers. You have data about the products your customers order. It's probably safe to assume that you might want to view data about a particular customer with a list of their orders and the ability to see the product details of each order. But if you are using SQL Reporting Services, there's no clean way to do this... Yes, you can create sub-reports. You can even create reports that let you drill down into the details of a child record, but with SRS that typically means the child detail report replaces the parent, leaving just a link to navigate back up the tree. But what you really want is to be able to click on an order and see the details on the same page. (This is for an online reporting solution... I'm not suggesting that you'll be able to click on a line in a printed report and have the data change. Just want to be clear about that.)</p> <p>Well, here's how you do it using Windows SharePoint Services (WSS) and SQL Reporting Services (SRS)... (If you're a developer, and you look at .Net 2.0 Web Parts and such, you'll probably realize that WSS is not necessarily a required piece, here, but it sure helps with the presentation... And it's free!) We'll also need some HTML and a bit of JavaScript, but I'll give you most of that. (I am going to assume that you know your way around the basics of WSS and SRS - if not, then you'll probably have to do a bit of reading up on that before you can implement this stuff.)</p> <p>The basics: We have a system with WSS installed and SRS installed in SharePoint integration mode. (This gives us a handy little Report Viewer Web Part that we use as the anchor for our dynamic report.) We also have a few reports:</p> <ul> <li>Customer Detail - Displays information about our customer: Name, address, customer number and so forth. <li>Customer Orders - Displays a list of orders for a particular customer. <li>Order Detail - Displays the details of a particular order.</li></ul> <p>Our desired end result for this example is a two paneled report. The upper panel showing the customer detail, along with a list of their orders, each displayed as a hyperlink. The lower panel showing the detail of any order that you click on.</p> <p>The first step is to create the parent report for the upper panel. (This is not a tutorial on creating SRS reports, there're plenty of them out there. I'm just giving you the basic chunks of what needs to happen.) This is actually two reports. You create a customer detail report and embed a customer orders report within it. You can develop and test this within Visual Studio. You'll most likely have the customer detail report accept the customer name or number as a parameter, then pass that along to the embedded orders report. (We're going to add some funky navigation to the orders report, but we'll come back to that.)</p> <p>Next, create an order details report that accepts an order number as a parameter.</p> <p>Now, let's move to WSS... As part of the SRS / SharePoint integration, you'll already have a document library for your reports - configure this doc lib as your deployment target in Visual Studio. We need a nice new Web Part page to act as the canvas for our dynamic report. For our project, I created a new document library called Dashboards that I used to collect these pages. I used the Web Part template that just has a single column of web parts the full width of the page.</p> <p>The first web part to add is the Report Viewer Web Part. Configure this guy to display the parent report - the one with the customer detail and embedded orders list. Below that, add a Content Editor Web Part. This is where a good part of the magic takes place. This Web Part is going to contain two important elements: An HTML iframe definition and a short JavaScript function to update the source attribute of the iframe. Make sure that when you edit the Content Editor Web Part, you use the Source editor, not the Rich Text editor. Here's what the contents of this Web Part will look like:</p> <p> <div style="padding-right: 5px; padding-left: 5px; font-size: 8px; padding-bottom: 5px; color: #ffffff; padding-top: 5px; font-family: courier; background-color: #80c0ff">Content Editor JavaScript:</div> <div style="padding-right: 5px; padding-left: 5px; font-size: 8px; padding-bottom: 5px; color: #000000; line-height: 20px; padding-top: 5px; font-family: courier; background-color: #ffffff"><script type="text/javascript"><br> function LoadOrderDetails(OrderNum)<br> {<br> document.getElementById('OrderDetail').src = <br> 'http://win2k3/reportserver?http://win2k3/ilm/reports/' +<br> 'OrderDetail.rdl&rs:Command=Render&rc:Toolbar=false' +<br> '&rc:Parameters=false&OrderNum=' + OrderNum;<br> }<br></script><br><br><iframe name="OrderDetail" scrolling="no" id="OrderDetail" src="" width="100%"></iframe></div> <p></p> <p>(Obviously you'll have to tweak that a bit - use URLs appropriate to your setup. You might have to play with the URL format a bit ,too, depending on where your SRS virtual directories get placed.)</p> <p>A quick overview of what this code does... It exposes a JavaScript function to the page. In this case, the function is called <strong>LoadOrderDetails().</strong> The function accepts a single parameter which, in our example is the order number for the order we need to display details about. We take this information and embed it into a URL that we assign to the src attribute of the iframe. The <strong>document.getElementById()</strong> bit is how we get a reference to the iframe. If you examine the URL that's being generated, you'll see that it points to the <em>reportserver</em> virtual directory, and passes along the URL to the report that we want displayed. That second URL has a series of parameters embedded into it. Most of them tell SRS how to display the report: basically, render it, hide the toolbar and hide the parameters window. The last item passed in the URL is the name of the parameter as defined in the report that we're calling. And we concatenate the value passed into the function as the value for that parameter. In our example, our OrderDetails report defines a parameter called "OrderNum". If we didn't include this in the URL, the report would expect to have the user type it in as they do in the parent report. But since we're hiding the parameters window and the whole point of this exercise if to eliminate the need for manually linking the reports, we include it as part of the call to display the report.</p> <p>The iframe definition is just below the script block. Make sure that your tokens match up! If you make a call to <strong>document.getElementById('OrderDetail')</strong> then make sure you assign "OrderDetail" as the value for the id attribute of the iframe.</p> <p>At this point, we have two pieces of the puzzle assembled. We have a report that displays the customer detail, along with a list of orders for that customer. We also have an iframe poised to display a specific report at the whim of a JavaScript function call. All we need to do now is connect them so the order details report displays the details of any order the viewer clicks on in the parent report.</p> <p>This final step is to configure the parent report to call the JavaScript function which updates the iframe source.</p> <p>In the report definition of the customer order report (that's the one embedded into the parent report), right click on the textbox that displays the order number itself and choose properties. you'll get a window that looks a bit like this:</p> <p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="413" alt="SRS Textbox Navigation Settings" src="http://www.camelogic.com/digitalcamel/DatabaseDependentDuoDeliversDazzlingDyna_A634/SRSTextBoxNav.jpg" width="487" border="0"> </p> <p>As you can see, on the navigation tab, you enter the call to the JavaScript function in the "Jump to URL:" box. The code there inserts the value of the OrderNum field of the report's datasource. When this is rendered in html as the report displays, it become a client side call to the JavaScript function we defined in the in the Content Editor Web Part in SharePoint.</p> <p>See? ... It's a piece of cake! Now each of the order numbers is a hyperlink that dynamically updates a sub-report displayed on the same html page.</p> <p>So let's go over the basic steps:</p> <ul> <li>Define a parent report in SRS that lists a collection of related values. (Customer Detail, Listing Order Numbers) <li>Define a child report that accepts a parameter to display further detail of an item. (Customer Order, Accepts Order Number) <li>Create a Web Part page to host the cooperative reports. (I like that... Cooperative Reports. I have coined a new term.) <li>Configure a standard SRS Report Viewer Web Part to display the parent report. <li>Configure a Content Editor Web Part with the appropriate JavaScript and iframe definition. <li>Configure the appropriate navigation property in the parent report to call the JavaScript function.</li></ul> <p>I truly hope that folks find this useful. I've seen quite a few inquiries on many forums about how to do something like this. Many people are surprised (as I was) to discover that SRS does not have this capability. But, then, it's a reporting engine, not a UI driven application.</p> <p>Here are a couple of things to note about this technique:</p> <ol> <li>Configuring the textbox to make the JavaScript call does not mean it will appear as a hyperlink when rendered. In the report definition, you'll have to format it in a way that catches the viewer's eye and let's them know they can click on it. (I typically go for the standard bright blue, underlined text.) <li>Setting this up creates a tightly bound relationship between these elements. You couldn't use this parent report anywhere that you don't also supply the JavaScript that it's expecting to call. (At least not without getting browser side errors.) <li>The parent report does not have to be very complex and it does not have to have an embedded sub-report. In one instance, I implemented a parent that does nothing but lets the viewer search for users in a directory with a search string. The resulting list of users are clickable links that update a user detail panel below the "search box". <li>You don't have to stop at one level of detail. For our ILM portal, I use a "search box" report to get a list of metaverse identities that match a search string. Click one to get details on the identity, including connector history and, in our case, a list of workflows that are associated with it. Click a workflow id and you get a rendered image of the current status of the workflow in another panel. Now, since the workflow image report is actually being called from within the iframe that the identity detail is displayed in, you have to scope the JavaScript call: <strong>="javascript:parent.LoadOrderDetails()"</strong> (and if you want another level of detail you may find yourself calling a parent of a parent...</li></ol> <p>Well, I certainly think that's quite enough to try and wrap your brain around for one sitting... Please let me know if you have success (or not) with this technique. And if you can add any more interesting angles, I'd love to see what you come up with.</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com2tag:blogger.com,1999:blog-4171068420734092267.post-82050411833867118452007-12-23T13:15:00.001-07:002008-08-15T23:17:55.849-07:00Uncovering the MIIStery of Attribute Level Deltas (In Holiday Verse)<p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="69" alt="lights12" src="http://www.camelogic.com/digitalcamel/UncoveringtheMIISteryofAttributeLevelDel_111A1/lights12_thumb.gif" width="204" border="0"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="69" alt="lights12" src="http://www.camelogic.com/digitalcamel/UncoveringtheMIISteryofAttributeLevelDel_111A1/lights12_thumb.gif" width="204" border="0"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="69" alt="lights12" src="http://www.camelogic.com/digitalcamel/UncoveringtheMIISteryofAttributeLevelDel_111A1/lights12_thumb.gif" width="204" border="0"></p> <div style="padding-left: 100px"> <p>'Twas the night before Christmas in The Keys rental house,<br>A blog must be written, so I warmed up my mouse.</p> <p>Wrote some new code in the SQL software,<br>in the hopes that our deltas will be processed with flair.</p> <p>No longer all changes, attributes instead<br>will be processed, as needed, as each row is read.</p> <p>Now sync'ing the data will be quick as a snap.<br>Clients will smile, they might even clap.</p> <p>Between data and metaverse we'll eliminate chatter.<br>('Though the view of the delta might get a bit fatter.)</p> <p>The results, very pleasing - make it known in a flash.<br>Must post on my blog. On the keyboard I'll mash.</p> <p>This is knowledge the field might be keen to know.<br>Visitor count on my site might even grow.</p> <p>So what are the details? You're eager to hear?<br>I'll tell you right now. (First a sip of my beer.)</p> <p>To process this way, had to think of a trick.<br>It had to be clever and it had to run quick.</p> <p>Keep it simple to implement, that was the game.<br>So that admins could run it and not go insane.</p> <p>"Now, WHERE clause! Now, LEFT JOIN! Now CASE WHEN and IF IN<br>On, INSERT! On, UNION!" (Some sweet code I'm mixin')</p> <p>To the top of the set: ADDs, DELETEs, yes, list all.<br>It's the modified ones that are different, ya'll.</p> <p>For each change in the row we must now specify<br>A new row in the table. I gave it a try.</p> <p>With all of the data, list the attribute, too.<br>That new little column is really a clue.</p> <p>This really streamlines the process, to tell you the truth.<br>Now ILM can focus on just what is new.</p> <p>Temp tables and queries in SQL abound,<br>'Til each little change in the data is found.</p> <p>(In the original poem, here, this line ends with "foot".<br>But to rhyme it in context... Couldn't think what to put...)</p> <p>On the code and technique I continued to hack<br>when finally all the results were on track.</p> <p>So I said to myself, "Good job, there, Jerry."<br>Stored procedure is where you must now, this code, bury.</p> <p>Input param'ters would be apropos<br>We must tell the script, after all, where to go.</p> <p>(Now here's another hard rhyme. This one, "teeth"<br>I'll fake and rhyme this line with the word "beef".)</p> <p>Okay, I admit it, that line was just silly.<br>But all of the lines in the poem I must filly.</p> <p>Alright, back on track, now, I tell myself,<br>This quality work won't just finish itself.</p> <p>The parameters, yes, this proc must be fed.<br>After all it can't read what's in your head.</p> <p>Provide the table and view names for this thing to work.<br>Then the code takes your data and just goes berserk.</p> <p>Nearing the end, whew, soon I can type prose.<br>Need to wrap this up cleanly, then, I suppose.</p> <p>If you have thoughts or comments, just give me a whistle.<br>To the comments you leave, I'll respond like <something that rhymes with whistle>.</p> <p>If this earns MVP for me, that's out of sight!<br>Time to end this in rhyme, so to all a good night.</p></div> <p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="69" alt="lights12" src="http://www.camelogic.com/digitalcamel/UncoveringtheMIISteryofAttributeLevelDel_111A1/lights12_thumb.gif" width="204" border="0"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="69" alt="lights12" src="http://www.camelogic.com/digitalcamel/UncoveringtheMIISteryofAttributeLevelDel_111A1/lights12_thumb.gif" width="204" border="0"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="69" alt="lights12" src="http://www.camelogic.com/digitalcamel/UncoveringtheMIISteryofAttributeLevelDel_111A1/lights12_thumb.gif" width="204" border="0"></p> <p>Yes, yes... 'tis a bit silly, but you read it all, didn't you? So stop your whining and let's get down to the gory details:</p> <p>The stored procedure is called <a href="http://www.camelogic.com/digitalcamel/spCreateAttributeLevelDeltaTable.txt" target="_blank">spCreateAttributeLevelDeltaTable</a>. Take that code and paste it into a new query window in SQL Management studio. (It'll look better once it's pasted there, too.) Run it to create the stored procedure.</p> <p>The proc takes four parameters:</p> <ul> <li>KeyColumn: The column you intend to use as the key for matching rows between the current and original data sources. <li>CurrentTable: The data source for the current version of the data. Table or view, doesn't matter. <li>OriginalTable: The data source for the original version of the data. Table or view, doesn't matter. <li>DeltaTable: The name of the table that will be created and filled with the delta information.</li></ul> <p>The delta table that you specify will be dropped and re-created each time the procedure is run, so don't get too attached to it. Add a call to this guy in the pre-processing stage of your delta sync script. And make sure that you drop (or at least truncate) the delta table after a <em>successful</em> delta sync to eliminate any redundant change processing. Here's what the call should look like:</p> <p> <table cellspacing="0" cellpadding="3" width="100%" border="0"> <tbody> <tr> <td style="color: white; background-color: #80c0ff" valign="top"><font face="Courier New" size="2">Call Syntax: <font face="Courier New" size="2">spCreateAttributeLevelDeltaTable</font></font></td></tr> <tr> <td style="color: black; background-color: white" valign="top"><font face="Courier New" size="2">EXEC [dbo].[spCreateAttributeLevelDeltaTable]<br> @KeyColumn = N'UniqueID',<br> @CurrentTable = N'tDataCurrent',<br> @OriginalTable = N'tDataOriginal,<br> @DeltaTable = N'tDataAttrDelta'</font></td></tr></tbody></table></p> <p>Obviously - well if it's not obvious, then this is all above your head, anyway - you can peruse the SQL code and get a feel for the technique I employed. You can also change the delta type keywords (ADD, DELETE, etc...) and column names to match your standard naming conventions. Then a few changes to the MA delta configuration through the MIIS UI and you're good to go.</p> <p>This is my <Insert Holiday Name Here> gift to you. Use it well and please let me know how it goes. (A little feedback wouldn't kill you, you know?)</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com1tag:blogger.com,1999:blog-4171068420734092267.post-42821681577651166192007-12-18T17:54:00.001-07:002008-01-08T15:15:02.456-07:00The Same, But Different<p>Okay... A short while ago I posted a little application I wrote called the MIIS Delta View Creator. It's neat and clean and does what it says it does. But I also have another method up my sleeve. (Well, it might be up my sleeve if I kept my sleeve in the hard drive bay...) Anyway, here's another version implemented as a SQL Management Studio template.</p> <p>Create the template:</p> <ul> <li>In SSMS, enable the Template Explorer (Ctrl + Alt +T) <li>If desired (and it is desired) create a new template folder. <ul> <li>Right click on the SQL Server Templates root folder and select "New" and then, yes... "Folder" <li>Type the new name for the folder. I suggest either "Rosencrantz" or "MIIS Custom", but this is all you.</li></ul> <li>Right click on your new template folder and create a new template. <ul> <li>I suggest any name other than, "New SQL Server Template". <li>I called mine, "Create Basic MIIS Delta View Components" and I have a reference to that in the comments of the script.</li></ul> <li>Now, right click your new template and select "Edit". <li>Paste <a href="http://www.camelogic.com/digitalcamel/CreateBasicDeltaViewComponents.txt" target="_blank">this code</a> into the query editor window and save the template.</li></ul> <p>Use the template:</p> <ul> <li>In SSMS, double click the template. <ul> <li>A copy of it will open in a new query editor window. <li>If you want to edit the template itself, right click it and select "Edit".</li></ul> <li>With the new query editor window as the active window, press Ctrl + Shift + M. <li>Enter the appropriate value for each parameter and click "OK". <ul> <li>You now have a script that, when run, will create the necessary delta view components.</li></ul></li></ul> <p>Okay... enough with the bullets.</p> <ul> <li>Maybe just one more...</li></ul> <p>The resulting script will create a basic delta view setup. All that's necessary to get started is to have one existing table/view. This table should represent the "Current" view of the data. The "Original" table and the delta view will be created. If there are objects with the specified names already in the database, they will be dropped!</p> <p>The script also creates a couple of additional components: A table copy stored procedure and a post process stored procedure. These components are useful for the way we implement a lot of the MIIS functionality. Read through the code and all will be revealed. If you have questions, comments or suggestions, please feel free to post them here.</p> <p>All I ask is: if my readers (yes, both of you) use this code, please note where you got it from in any altered versions that you mangle (create).</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com0tag:blogger.com,1999:blog-4171068420734092267.post-51386454866699402502007-12-08T00:49:00.001-07:002008-08-15T23:10:34.740-07:00Cider, Workflows and Just Enough Knowledge...<h6></h6> <p>As I recall, it was out in the country somewhere... The kind of place you can't find without having been there before. The sweet, musty scent of a single room log cabin that hasn't seen a carbon based life form larger than a raccoon for well over 20 years. The only light coming from the soft blue glow of the power L.E.D from the wireless access point... Hmm... Wait a second. I'm not remembering quite right... Might have been a room at the DoubleTree in San Jose.</p> <p>On the road and armed with Peanut M&M's, a case of Hornsby's Hard Apple Cider, and some book we'd just purchased from the local techno-geek bookshop, <a href="http://www.identitychaos.com/" target="_blank">Mr. Turner</a> and I decided to implement our first Windows Workflow Based solution at a client. Start simple, was our motto. So we did. We took one of simplest concepts we could come up with and decided to implement it in the most complex and convoluted way possible. All in the name of progress. We had chosen to build a workflow based delayed events Management Agent for MIIS.</p> <p>Brad's knowledge of the inner workings of MIIS is intense, and I have the ability to write code so concise that a popular compression algorithm, in a fit of jealousy and frustration, locked itself in the inner sanctum of my display driver and refuses to come out. (The proof is in the dead pixel in the middle of the screen on my laptop.) And while the intricacies of marrying these two skill sets have evoked solicitations from the Hollywood screenwriting elite, that's not what this blog is about. That's just a bit of background. The history. A peek behind the wizard's curtain, if you will...</p> <p>What I gathered you all here to talk about is this: Never drop a goldfish into a glass of vodka. Okay? No, not even if it's the good vodka like the kind that comes in the fancy frosted bottle with that guy's face that you can see through the small patch of clear glass on the <em>inside</em> of the other side of the bottle. (How do they get that guy in there?)</p> <p>And speaking of goldfish, here's a little story on how I was blind-sided by a .Net assembly versioning conflict...</p> <p>I did build that workflow engine. 'Twas my first foray into the world of Windows Workflow Foundation and I had just enough knowledge to make it all work, but not enough to understand what I was doing. The solution consisted of four projects: The workflow engine service, the workflows assembly, a utility project and a setup project. The workflows, themselves, were pretty simple: Delay, Notify, Delay. Short and sweet. The workflow engine was implemented as a service and contains the standard SQL based persistence service, the standard tracking service and an External Data Exchange service to allow the workflows to notify the host of certain events. All of the versioning was set to auto increment the build and revision.</p> <p>After creating the workflows and getting the service built and running and everything tested to a point of satisfaction, the service was rolled out and a production WorkflowMA was born. And it was good. But not good enough. Had to tweak the service a bit and ended up having to deploy a couple of replacement versions.</p> <p>These were long running workflows, delaying for up to 90 days at a time. So it was quite a while before I was enlightened unto the error of my ways. After a while we realized that things were not processing as they did in testing. (You see, in testing, I used 90 seconds, not 90 days. Deadlines, you know...) So I'll skip past the details here and get to the stuff that matters...</p> <p>I used strong named assemblies. That's important. If I hadn't, I might not have had any of these issues. But then I couldn't have deployed signed code, either.</p> <p>What was happening was that workflows were getting stuck in the persistence service. While I didn't make changes to the workflows, I had made updates to the service. But I always redeployed the full setup. And when I did a full solution recompile, I inadvertently changed the version numbers of the workflows. So when the persistence service tried to re-hydrate them, it failed as the appropriately versioned workflow classes were not available. .Net will not automatically use a newer version of a strong named assembly. They just sat there in the persistence store. Orphans.</p> <p>How to fix it? Well, my first attempt at a workaround was to use a binding redirect in the app.config:</p> <p></p> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td style="color: white; background-color: #80c0ff" valign="top">Code</td></tr> <tr> <td style="color: black; background-color: white" valign="top"><font face="con" size="2"><configuration><br> <runtime><br> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"><br> <dependentAssembly><br> <assemblyIdentity name="MIISWorkflowLib" publicKeyToken="123abc456def7890" culture="neutral" /><br> <bindingRedirect oldVersion="1.0.0.0-1.0.9999.0" newVersion="1.1.0.0" /><br> </dependentAssembly><br> </assemblyBinding><br> </runtime><br></configuration></font></td></tr></tbody></table> <p></p> <p>Just specify a version range big enough to cover all the previous versions and redirect them to the one new and latest version. </p> <p>Awesome! The old workflows rehydrated using the new assembly. But then the tracking service complained:</p> <blockquote> <p><font face="Consolas" size="2">Value cannot be null. Parameter name: profile</font></p></blockquote> <p>There was a mismatch in the profile information in the serialized workflow and the tracking database records. Another exception. It was like the parents had come back to claim their orphaned little workflow, but they didn't have proper ID. Couldn't prove they were they rightful owners, so the WWF authorities intercepted the happy reunion, again leaving the workflow cold and naked in the persistence store. (Why naked, you ask? More dramatic.) </p> <p>How to fix it? Well, you can look at the tracking database (examine the WorkflowDefinition column of the Workflow table) and see the version numbers of the workflows that it's cataloged. (If the WorkflowTypeID doesn't match any records in the WorkflowInstance table, you can probably skip that version. No workflows were actually created from that assembly.) Recompile a workflow assembly for each version, updating the AssemblyVersion before compile and copying the compiled assembly to a subfolder structure under the host's startup folder. Then use codebase hints in the app.config file to tell the host where each version of the assembly lives. (For some reason, I didn't use the GAC. If you do, you can just dump each version there and be done with it. But my solution required more typing, so it must be better.) <p>Folder Hierarchy: <p align="left"><img height="274" alt="Folder Hierarchy" src="http://www.camelogic.com/digitalcamel/CiderWorkflowsandJustEnoughKnowledge_EADE/FolderHierarchy.png" width="535"> </p> <p align="left">app.config:</p> <p></p> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td style="color: white; background-color: #80c0ff" valign="top">Code</td></tr> <tr> <td style="color: black; background-color: white" valign="top"><font face="con" size="2"><configuration><br> <runtime><br> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"><br> <dependentAssembly><br> <assemblyIdentity name="MIISWorkflowLib" publicKeyToken="123abc456def7890" culture="neutral" version="1.0.2701.23729" /><br> <codeBase version="1.0.2701.23729" href="Workflows\1_0_2701_23729\MIISWorkflowLib.dll" /><br> </dependentAssembly><br> <dependentAssembly><br> <assemblyIdentity name="MIISWorkflowLib" publicKeyToken="123abc456def7890" culture="neutral" version="1.0.0.0" /><br> <codeBase version="1.0.0.0" href="Workflows\1_0_0_0\MIISWorkflowLib.dll" /><br> </dependentAssembly><br> </assemblyBinding><br> </runtime><br></configuration></font></td></tr></tbody></table> <p></p> <p align="left">Awesomer! The workflows can re-hydrate <em>and</em> that finicky tracking service is no longer complaining. But then one more little demon wielded its ugly head. The ExternalDataExchange service. See, I was using that to allow the workflows to chat with the host application. When I put this project together I only had one workflow assembly. I added the required interface definition to that project and just referenced it from the workflow host application. In Visual Studio, I set a reference to the workflow project in the host application project. This allowed me access to that interface. But post-fix there were multiple versions of the workflow assembly. You can't easily reference more than one assembly with the same name. (With Reflection, all things are possible... Well, many things.) And I needed a reference to an interface my class would implement, not a class already defined in the other assembly.</p> <p>How to fix it? Well, this took me while to figure out. Not because it's an especially difficult problem, really, but because I expected it to be. And if you're not looking for a simple solution, I can promise that you won't find one. All I had to do here was separate the interface definition from the workflow definition. I couldn't add the interface to the host app because the host app already had a reference to the workflow app. If I put the interface there, the workflow app would need a reference to the host app. That's what they call a <em>circular dependency</em> and, in some states, that's a felony. (Well, Visual Studio won't let you do it, anyway.) So enter project number five, consisting of only the interface definition. Reference the new project from both the host application and the workflow assembly and... Viola! A complete and working solution. (Awesomest!)</p> <p>Now... The real lesson here is not how to fix this situation - it's that you should avoid it all to begin with. I was so focused on messing with the new workflow gizmos that I just didn't think through the peripheral .Net stuff. Lesson learned and In the solution, now, the workflow assembly project no longer auto-increments it's version. Yeah... It would have been that simple.</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com2tag:blogger.com,1999:blog-4171068420734092267.post-79166476181232348712007-12-04T17:34:00.000-07:002008-01-08T15:13:00.655-07:00Delta Dawn...<p>Hey, there... How'ya doin'? Good... Good... Been a while, I know, but I finally have something new to cast off into the blogoshpere. Here's my first attempt at shining some light on creating MIIS delta views. Well, perhaps "<em>shining some light on</em>" is not the proper metaphor, but it does tie in nicely with the title and, yes... the <a href="http://www.camelogic.com/digitalcamel/DarknessOnTheDelta.mp3" target="_blank">theme music</a>. (That's good old fashioned Barbershop harmony, that is. If you're into that sort of thing, there's a lot more of it <a href="http://www.barbershop.org/web/groups/public/documents/pages/pub_jukebox.hcst#free" target="_blank">here</a>. Don't be shy, gentlemen, seek out your local chapter of the <a href="http://www.barbershop.org" target="_blank">Barbershop Harmony Society</a> today!)</p> <p>Introducing the <a href="http://www.camelogic.com/digitalcamel/MDVCSetup.msi">MIIS Delta View Creation Wizard</a>! <font size="1">(version 0.9 - beta type software)</font> A handy little utility to automate the creation of standard delta views. The interface is pretty straight forward:</p> <p><img src="http://www.camelogic.com/digitalcamel/DVCW0.9.jpg"> </p> <p>Download it. Try it. Give it as a unique holiday gift. And by all means, please post your comments and suggestions.</p> <p>"When there's darkness on the delta..." use the MDVC and brighten up your day! (If you don't get that, you didn't listen to the theme music.)</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com0tag:blogger.com,1999:blog-4171068420734092267.post-21702999583501012142007-08-10T21:22:00.001-07:002007-08-10T21:22:49.530-07:00I Need More Minutes<p>I lied... I had intended to write my next entry on some user interface details with the .Net 2.0 GridView, but I haven't gotten around to it yet. I need more minutes. So many topics... So little time. I wish life was more like a mobile phone plan. Well, in one particular way, at least.</p> <p>Let's face it, for the most part, the way mobile phone providers and hardware vendors are tied together in this country - that's the USA for anyone reading this from abroad... Well, abroad from my perspective... - makes for annoying service contracts on the service provider side and a stifling of creativity on the hardware vendor side. But none of that is my point. </p> <p>The one thing I wish I could get in life is more minutes. For a fee I can have minutes added to my cellular phone contract. I can use 'em, for the most part, whenever I want. Or not use them at all. It's my option. But I can request minutes.</p> <p>I end up with so many ideas and interests that I want to pursue, there's so much time that needs to be dedicated to implementing a client solution properly and let's not forget about the wife and child, they deserve their time, too. (Although Mufasa, the aforementioned child - an overly affectionate, tiny version of a lion - has no issue strolling up and sprawling himself across my keyboard or perching on my shoulder when he feels neglected...) The only way to accomplish it all would be with more minutes. I'd be willing to pay, no doubt. More minutes. I love the sound of that.</p> <p>"Honey... I know you wanted to go camping this weekend and we still need to bathe the office and paint the cat, but I have to finish this project before Monday and I have to do it from the client's site." You know that's not going to win you any points on the home front. But if you could just add more minutes...</p> <p>My current life service provider offers 60 minutes each hour, and I use every one 'em. I want to upgrade my plan so I can go camping.</p> <p>Imagine... Add an additional 30 minutes per hour during peak periods and get a bonus of 500 extra night and weekend minutes. I could finish my project <strong><em>and</em></strong> enjoy an extended camping trip with my family.</p> <p>And consider this: if I was Hindu, perhaps I could get that rollover plan and enjoy some of my unused minutes next time around...</p> <p>Yeah... I definitely need more minutes.</p>rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com0tag:blogger.com,1999:blog-4171068420734092267.post-79752158803078911952007-07-28T15:10:00.001-07:002008-01-08T15:12:05.492-07:00Filtering data with the ASP.NET 2.0 GridView<p>Finally, something else to blog about... Are you happy, Brad?</p> <p>In implementing a full Identity Management solution, there are times when you need to supply the customer with some external utilities. In this case, we needed a simple interface to allow the customer to manage a list of location codes, mapping them to Active Directory OUs and login script paths. We decided that, in this case, the best solution was a simple database table and a web interface to update the information.</p> <p>ASP.NET, especially with it's more recent incarnations, provides some mechanisms for building simple sites with little more than drag and drop in Visual Studio. But as you try to build in a bit more functionality, you find that you sometimes have to tweak the built-in stuff. (Our tweaking, here, is very simple - it's just a matter of adding the proper code to the proper events.)</p> <p>The LocationCodes mini-project found me banging my head against the wall (drywall repairs: $73.26) trying to implement something as simple as a filter for narrowing down the list of items on the page. Here's what I discovered through a combination of posting, searching and just plain messing around with it. It's actually pretty simple...</p> <p>First thing to note is that since a GridView merely displays the record set that it's given, filtering actually takes place in the DataSource control it's bound to. Cool - the Datasource control has a "Filtering" event! How much simpler could it be...? Well, just to mess with you, the "Filtering" event of the DataSource is exactly where you don't place the code. The filtering event only triggers if the DataSource's filter property already has a value.</p> <p>Here's our scenario: We have a web page with a DataSource, a GridView and a couple of Buttons and a TextBox for dealing with the filter.</p> <p>Here's the event firing order for the first time the page loads (We have no filter set yet.):</p> <table cellspacing="0" cellpadding="1" width="400" border="0"> <tbody> <tr> <td style="color: black; background-color: white" valign="top" width="131">Object</td> <td style="color: black; background-color: white" valign="top" width="267">Event</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">PreInit</td></tr> <tr> <td valign="top" width="131">DataSource</td> <td valign="top" width="267">Init</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">Init</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">Init</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">InitComplete</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">PreLoad</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">Load</td></tr> <tr> <td valign="top" width="131">DataSource</td> <td valign="top" width="267">Load</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">Load</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">LoadComplete</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">PreRender</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">DataBinding</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">RowDataBound (Once for each row.)</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">DataBound</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">PreRender</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">PreRenderComplete</td></tr> <tr> <td valign="top" width="131">DataSource</td> <td valign="top" width="267">Unload</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">Unload</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">Unload</td></tr></tbody></table><br> <p>When a filter is set, or cleared, or when an item is edited, the order of events is slightly different because we have to react to what the users intent is, meaning that we'll trigger some events programmatically.</p> <p>Eliminating most of the events that don't really effect us, here are the events that occur when a user interacts with the page:</p> <p>When a filter is entered in the TextBox and the user clicks the Filter button:</p> <table cellspacing="0" cellpadding="1" width="400" border="0"> <tbody> <tr> <td style="color: black; background-color: white" valign="top" width="131">Object</td> <td style="color: black; background-color: white" valign="top" width="267">Event</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">Load</td></tr> <tr> <td valign="top" width="131">DataSource</td> <td valign="top" width="267">Load</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">Load</td></tr> <tr> <td valign="top" width="131">Filter Button</td> <td valign="top" width="267">Click (Set Filter, Call DataSource.DataBind)</td></tr> <tr> <td valign="top" width="131">DataSource</td> <td valign="top" width="267">DataBinding</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">LoadComplete</td></tr> <tr> <td valign="top" width="131">DataSource</td> <td valign="top" width="267">Filtering</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">DataBinding</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">RowDataBound (Once for each row.)</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">DataBound</td></tr> <tr> <td valign="top" width="131">DataSource</td> <td valign="top" width="267">Unload</td></tr> <tr> <td valign="top" width="131">GridView</td> <td valign="top" width="267">Unload</td></tr> <tr> <td valign="top" width="131">Page</td> <td valign="top" width="267">Unload</td></tr></tbody></table><br> <p>When the button is clicked, we set a Session variable to hold the filter text and force the DataSource to bind. (For readability, the code snips presented her don't include the full method signatures. But Visual Studio creates those for you anyway...)</p> <p><pre class="code"><span style="color: rgb(200,200,100)">Protected Sub btnFilter_Click(...) <br> Session("FilterExp") = txtFilter.Text<br> LocationCodesDS.DataBind()<br>End Sub</span></pre><br><br /><p>In the DataBinding event, we check to see if there is any filter text. If so, then set the FilterExpression property of the DataSource. If there is no filter (the Session variable is not set) then clear the FilterExpression property:</p><br><br /><p><pre class="code"><span style="color: rgb(200,200,100)">Protected Sub LocationCodesDS_DataBinding(...)<br> Dim f As String = Session("FilterExp")<br> <br> If (f Is Nothing) Then<br> LocationCodesDS.FilterExpression = ""<br> Else<br> LocationCodesDS.FilterExpression = _<br> "LocationCode Like '" & f & "%'"<br> End If<br>End Sub</span></pre><br><br /><p>When the FilterProperty of the DataSource is set, the text in the FilterExpression is basically added to the DataSource's SQL statement as a WHERE clause. If the FilterExpression of the DataSource is set, then the Filtering event of the DataSource will be triggered. This is where you can validate the filter expression and intercept the filtering process (e.Cancel = True). The way I implemented this, I would check the filter expression in the Filter Button's click event. But if you bound the DataSource's FilterExpression property to the TextBox directly, you'd need a way to stop the filtering process if, by chance, a user entered something that might be considered invalid or even malicious when attached to the underlying SQL.</p><br><br /><p>Since the click event of the Filter button doesn't run every time the page loads (there are other ways to interact with page besides that one button) we need to tell the DataSource about any potential filters every time we have to rebuild the page. The DataSource's Init event is fine place to do this. Without this, if a user, say, clicks to edit a particular row, the DataSource will load the full record set on the postback and the GridView's row pointer will very likely end up pointing to a different row, causing the user to edit the wring data. The code is simple:</p><br><br /><p><pre class="code"><span style="color: rgb(200,200,100)">Protected Sub LocationCodesDS_Init(...)<br> If Session("FilterExp") <> String.Empty Then<br> LocationCodesDS.DataBind()<br> End If<br>End Sub</span></pre><br><br /><p>VB .NET is kind enough to allow String.Empty to be compared to an actual empty string or to VB's null equivalent of 'Nothing'. Thus allowing us this comparison even if the Session variable doesn't exist. Conversion of this bit of code to C# needs to take this into account.</p><br><br /><p>The last bit of functionality required for our basic filter implementation is the ability to clear the filter. We do this in the Click event of the ClearFilter Button:</p><br><br /><p><pre class="code"><span style="color: rgb(200,200,100)">Protected Sub btnClearFilter_Click(...)<br> Session.Remove("FilterExpression")<br> LocationCodesDS.DataBind()<br> txtFilter.Text = ""<br>End Sub</span></pre><br><br /><p>That's it for basic filtering. Next time, we'll talk a bit more about the GridView and how to work with it in regards to maintaining a proper user interface...</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com4tag:blogger.com,1999:blog-4171068420734092267.post-15700014034905553062007-05-23T12:24:00.001-07:002007-12-02T18:52:03.283-07:00After Duress<p>Okay... So I'm new to this blogging thing and a couple of days after my first post I realized that I forgot to mention something that is directly related.</p> <p>But what's the protocol for blog updates? Technically, this isn't a new subject. How sacred are the posted blogs? Can one alter a blog once posted without disturbing the natural order of things? And since this is all digital, is the natural order simply 0 then 1? I mean there's not much room for disturbing that particular natural order short of completely reversing it. And certainly I don't want to be responsible for an alteration of <em>that</em> magnitude.</p> <p>But I digress... Or not, actually, since I haven't even started discussing the thought I was thinking before I was confronted with the whole blog alteration conundrum. So now I <insert opposite of digress, here>...</p> <p>In discussing the issue of certificate revocation and the delays caused by automatic verification of certain, signed .Net assemblies, I forgot to mention that it seems the verification process is intermittent. When I'm on an airplane (as I am now, not that it matters) and I start up SQL Server Management Studio, I don't get that long delay we experienced with disconnected MIIS server.</p> <p>So it seems that either the certificate is being verified against a published crl, or the system is updating it's local copy of the crl or something along those lines, and once verified, or updated, it's good for some amount of time. So it's possible that the issue can be solved by simply opening ports through the firewall to allow the system a quick peek to the published crl to satisfy its curiosity for a while. Once sated, the firewall can be locked down again until the system loses confidence and requires another gander at the certificate black list.</p> <p>This is all speculation on my part, however. I haven't verified this other than watching the behavior of my own system. And digging deeper into this isn't too high on my priority list. But if I do stumble across a definitive answer to this, I'll be sure to post it.</p> <p>Or do I just update this post...?</p> rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com0tag:blogger.com,1999:blog-4171068420734092267.post-74757425328721951952007-05-22T01:11:00.001-07:002008-01-08T15:08:32.614-07:00Under Duress<p>At the last minute and at great expense, the Frobozz Magic Blog company (How many people will get <em>that</em> reference...?) is proud to present... My blog.</p><p>For quite some time I've been intending to start a blog, if for no other reason than to have an easily accessible repository of useful bits and pieces that I can get to from pretty much anywhere. Call it an information wallet, a portable brain or perhaps auxiliary memory, basically a place to keep all that information I can't remember just at the time I need to remember it. I'd get to it one day - if, in a moment of spare time, I could just remember to do it.</p><p>But my good friend and colleague, <a title="Brad's Blog" href="http://idchaos.blogspot.com/" target="_blank">Brad Turner</a> - with an evil grin on his face (I know that for a fact, as I was sitting next to him when he did this...) - has forced my hand, well both hands, actually, since I use both when typing... His <a title="Brad's evil blog post..." href="http://idchaos.blogspot.com/2007/05/miis-sp2ilm-2007-timeouts-out-of-memory.html" target="_blank">recent post</a> regarding an issue we were troubleshooting has an embedded link to what was my non-existent blog. So out of fear of Internet wide humiliation, I was forced to blog my first blog in the blogosphere.</p><p>So here's the actual blog part of this blog post...</p><p>Since most of you probably got here from the link on Brad's post, I'll not rehash the entire situation. But if, by chance, you happened upon this post via some other circuitous route, here's the basic summary:</p><p>While building a decidedly kick-ass identity management solution for a client, we ran across a situation with one of the servers in which it appeared a bit unresponsive at times and, in general, just didn't behave in a fashion similar to it's fail-over brother in a far away data center, despite being built as a virtual twin. The main symptoms were lethargic application startups and curious memory errors delivered by <a title="MIIS @ Microsoft" href="http://www.microsoft.com/miis/default.mspx" target="_blank">MIIS</a>. After numerous troubleshooting attempts, staring stupidly at the monitor and asking questions along the lines of, "What the...?" we did finally figure out the problem.</p><p>In a nutshell, the problem was that the system was trying to verify certificates associated with certain applications, but that particular server did not have access to the Internet. So applications, in this case SQL Server Management Studio, take some time to startup because they're waiting for the timeout in trying to access the Microsoft Certificate Revocation List at http://crl.microsoft.com.</p><p>In the case of MIIS, the delay caused MA extension timeouts and seemingly unassociated out of memory errors.</p><p>There are a few ways to mitigate this issue. We chose, at least for now, to disable certificate verification through the advanced properties in Internet Explorer.</p><p>All of the details, including Event Log entries, etc. are in <a title="Another link to Brad's evil post..." href="http://idchaos.blogspot.com/2007/05/miis-sp2ilm-2007-timeouts-out-of-memory.html">Brad's post</a>, so if you haven't been there yet, go now.</p><p>Go on...</p><p>That's all I have to say for now...</p><p>No reason to hang out here anymore...</p>rlrcstrhttp://www.blogger.com/profile/08278317100165920232noreply@blogger.com0