Recipe 11.2. Using Server Controls and User Controls as Web Parts


Problem

You want to take advantage of web part functionality while leveraging standard ASP.NET server controls or perhaps your existing user controls.

Solution

Use the ASP.NET 2.0 membership and web part features by implementing the membership features described in Recipe 9.5 and modify web.config to configure the web part provider. In the pages that use web parts, add a WebPartManager control, add one or more WebPartZone controls, add a CatalogZone control, and add the desired server controls and user controls to the CatalogZone.

Add a <webParts> element to web.config as follows:

 <webParts> <personalization defaultProvider="AspNetSqlPersonalizationProvider"> <providers> <remove name="AspNetSqlPersonalizationProvider"/> <add name="AspNetSqlPersonalizationProvider" type="System.Web.UI.WebControls.WebParts.SqlPersonalizationProvider" connectionStringName="sqlConnectionString" applicationName="CH11ExamplesVB" /> </providers> <authorization> <deny users="*" verbs="enterSharedScope" /> <allow users="*" verbs="modifyState" /> </authorization> </personalization> </webParts> 

In the .aspx file for the pages that will use web parts:

  1. Add a WebPartManager control.

  2. Add one or more WebPartZone controls.

  3. Add a CatalogZone control along with PageCatalogPart and DeclarativeCatalogPart controls.

  4. Add the server controls and user controls you want to use as web parts to the DeclarativeCatalogPart control.

  5. Add a Customize button (or equivalent) to provide the ability for the user to initiate customization of the page.

  6. Optionally add a Reset button (or equivalent) to provide the ability to reset all page customizations.

In the code-behind class for the pages using web parts, use the .NET language of your choice to:

  1. Implement an event handler for the Customize button click event and set the WebPartManager DisplayMode to CatalogDisplayMode to allow the user to customize the page.

  2. Optionally implement an event handler for the Reset button click event and call the ResetPersonalizationState method to reset all user customizations.

The application we have implemented to demonstrate the solution is shown in Examples 11-1, 11-2, 11-3, 11-4, 11-5, 11-6, through 11-7. Example 11-1 shows a user control that displays the weather for Charlottesville, Virginia. Examples 11-2, 11-3 through 11-4 show the .aspx and code-behind for a user control that displays a list of books from a database. Examples 11-5, 11-6 through 11-7 show the .aspx and code-behind for a page that demonstrates using standard controls and user controls as web parts.

Figure 11-1 shows the demonstration page as it is originally displayed before any customization. Figure 11-2 shows the demonstration page in catalog display mode with a calendar added to the first WebPartZone. Figure 11-3 shows the demonstration page after customization.

Discussion

Many applications need the ability to allow users to control what content is presented on a page as well as to position the content in the location of their choosing. This functionality has been available with portal applications for many years but has been beyond the reach of most developers because of the high cost of off-the-shelf packages to support portals or the high cost of rolling your own.

Figure 11-1. Demonstration page before adding content


Figure 11-2. Demonstration page in catalog display mode


ASP.NET 2.0 includes a web parts framework that provides the ability to build your own portal applications to allow users to customize the content, appearance, and behavior of pages. For all but the most complex applications, little code is required to implement a portal-style application.

Figure 11-3. Demonstration page after adding content


Like most other features in ASP.NET 2.0, the web parts framework is built using the provider model. One provider for the framework is shipped with 2.0: the SqlPersonalizationProvider provider. Like many of the other providers, it supports using SQL Server 7 or later versions to store and retrieve the web part personalization information. If you need to use a different data store for your application, you can create your own provider.

Microsoft uses the following terminology in ASP.NET 2.0:


Membership

Describes the authentication features


Role Manager

Describes the authorization features as a function of a user's roles


Profile

Describes the features used to store information about a user


Personalization

Describes the personalization that can be done with web parts


Microsoft recognized that web parts would be more attractive to developers if they were able to use standard controls and reuse user controls that had been developed.

As part of the web part framework, support for using any standard, custom, or user control as a web part is provided. This is accomplished by automatically wrapping these "standard" controls with a GenericWebPart object at compilation time. This way, all of your investment in controls of all types is preserved and any of these controls can be used as web parts with no modification.

Using standard server controls and user controls as web parts may not always be the best solution, because of a few limitations on what you can do with the controls. Refer to Recipe 11.3 and Table 11-1 for more information on the limitations.


In our application that implements this solution, we have implemented the Membership functionality for authentication and authorization, as described in Recipe 9.5, to provide the unique identification of the user for the personalization data. The use of the Membership features in ASP.NET 2.0 is not required to use web part personalization. What is required is to provide a unique value for each user in the User.Identity.Name property of the Principal object used for authentication, which is needed by the SqlPersonalizationProvider to identify the personalization data for the user. This means, for example, that the authentication solution provided in Recipe 9.1, which does not use Membership, can be used equally well.

The Membership and Personalization providers share many tables in the database. If you want to use the Personalization feature but do not intend to use the Membership features, you still need to add the tables required for the Membership and Personalization providers. Refer to the "Using SQL Server Instead of SQLExpress with the Membership and Role Providers" sidebar in Recipe 9.5 for information on creating a database with the required tables or adding the required tables to an existing database.


Web part personalization requires authenticated users. You cannot enable personalization for anonymous users. Attempts to use personalization with anonymous users will result in an exception being thrown when the user tries to customize the page. Several different exceptions are thrown, depending on which operation is performed. All of the exception messages indicate that personalization is not enabled and/or modifiable.


Once an authentication mechanism is in place, the next step is to configure the web part framework by adding the following <webParts> element to web.config. The connnectionStringName must be set to the name of the connection string defined for your database in web.config, and applicationName should be set to a unique name for your application. The applicationName is used by the SqlPersonalizationProvider to identify the data for your application in the database and to allow multiple applications to share the same database while keeping the personalization data separated.

 <webParts> <personalization defaultProvider="AspNetSqlPersonalizationProvider"> <providers> <remove name="AspNetSqlPersonalizationProvider"/> <add name="AspNetSqlPersonalizationProvider"  type="System.Web.UI.WebControls.WebParts.SqlPersonalizationProvider"  connectionStringName="<connection string>"  applicationName="<your application name>" /> </providers> <authorization> <deny users="*" verbs="enterSharedScope" /> <allow users="*" verbs="modifyState" /> </authorization> </personalization> </webParts> 

The <remove> element is used to remove a previous definition of a provider with the same name. Attempts to add a provider with a name that has been defined will result in an exception being thrown. If you always remove the provider's name prior to adding a provider, you will never have to deal with the problems that can occur with colliding names in an application that uses multiple web.config files.


Next, you need to add the web part controls to your .aspx file. The first control to add is a WebPartManger control. The WebPartManager control is responsible for managing all other web part controls on the page. The WebPartManager control must be added inside the <form> element and before any other web part controls.

Only one WebPartManager control can be placed on a page. Otherwise, a parsing error will occur.


Now you need to add one or more WebPartZone controls. WebPartZone controls define the area(s) on a page where web parts can be placed. To control the page layout better, WebPartZones are frequently placed in tables. In our application, we have defined a table with three rows. A WebPartZone control is placed in the first and second rows of the table.

The final control to add is a CatalogZone control. The CatalogZone control is responsible for managing the user interface that displays the available web parts and provides the user the ability to select web parts and add them to WebPartZones. The CatalogZone control is not visible in the page until the WebPartManager is placed in the CatalogDisplayMode (described below). In our example we have added the CatalogZone control to the third row in the table.

Within the CatalogZone control, a <ZoneTemplate> element is added along with a PageCatalogPart control and a DeclarativeCatalogPart control.

The <ZoneTemplate> element is a container for other catalog part controls. It provides the ability to define CatalogPart controls declaratively.

The PageCatalogPart control is responsible for managing web parts that have been added to the page but have been closed by the user (more about closing web parts later). It provides the list of closed web parts and gives the user the ability to return them to a WebPartZone.

The DeclarativeCatalogPart control contains a <WebPartsTemplate> element that acts as a container for standard server controls, user controls, and custom web parts available to the user. The DeclarativeCatalogPart control provides the ability for a developer to define declaratively the controls available to the user.

Here is the complete CatalogZone control for our application:

 <asp:CatalogZone  runat="server"  EmptyZoneText="No Catalog Items"  HeaderCloseVerb-Visible="false"  Css  Padding="6" > <ZoneTemplate> <asp:PageCatalogPart  runat="server" Title="Previously Closed Controls" /> <asp:DeclarativeCatalogPart  runat="server" Title="Available Parts" > <WebPartsTemplate> <ASPNetCookbook:CvilleWeather   runat="server"  Title="Weather" /> <asp:Calendar  runat="server"  Title="Calendar" /> <ASPNetCookbook:BookData  runat="server" Title="Book Data" /> </WebPartsTemplate> </asp:DeclarativeCatalogPart> </ZoneTemplate> </asp:CatalogZone> 

In our application, we have included Customize and Reset buttons in the .aspx file. The Customize button is used to initiate the customization of the page. When the user clicks the Customize button, the btnCustomize_Click method in the code-behind is called and sets the DisplayMode property of the WebPartManager to CatalogDisplayMode. This causes the CatalogZone control to be visible on the page, which in turn provides the user the ability to add and delete web parts on the page. With Internet Explorer 5. 5 and later versions, as well as some other browsers, the user can drag web parts to different locations within a WebPartZone as well as between WebPartZones.

 

wpm1.DisplayMode = WebPartManager.CatalogDisplayMode

wpm1.DisplayMode = WebPartManager.CatalogDisplayMode;

The Reset button provides the ability to reset all personalization and return the page to its original state (see Figure 11-1). When the user clicks the Reset button, the btnReset_Click method in the code-behind is called and the ResetPersonalizationState method of the WebPartManager's Personalization object is then called causing all personalization data for the page for the current user to be reset.

 

wpm1.Personalization.ResetPersonalizationState()

wpm1.Personalization.ResetPersonalizationState();

When web parts are displayed in a page, a title bar is added to the control being used as a web part. The title bar includes a title and buttons to minimize or close the web part. A calendar control used as a web part is shown in Figure 11-4.

Figure 11-4. Calendar control displayed as a web part


When the Minimize button is clicked, the web part is collapsed on the page with only the title bar displayed, as shown in Figure 11-5. The Minimize button is then changed to a Restore button, as shown in Figure 11-6.

Figure 11-5. Web part minimized


Figure 11-6. Web part minimized, showing available buttons


When the Close button is clicked, the web part is removed from the page but is not deleted. It still exists and has been added to the PageCatalogPart control described above. It can be added back to the page by clicking the Customize button, selecting the Previously Closed Controls catalog, selecting the desired closed control, and adding it to the desired zone.

The buttons in the web part titlebar can be displayed as a menu, as in this example, or as fixed buttons in the titlebar by setting the WebPartVerbRenderMode attribute of the WebPartZone control to TitleBar. The available options are Menu and TitleBar.

When the buttons are displayed as a menu, ASP.NET generates DHTML for the menu. If the requester's browser does not support the required DHTML, ASP.NET will revert to displaying the buttons in the header.


ASP.NET 2.0's web part framework provides the ability for any application to use web parts and to support portal functionality with little work. This example has only touched on some of the functionality available with web part personalization. Refer to the other recipes in this chapter as well as the WebPartManager class in the MSDN Library for more information on what is possible with the web part framework.

See Also

Other recipes in this chapter, Recipe 9.5, and the MSDN Library for information on the WebPartManager class

Example 11-1. User control to display the local weather (.ascx)

 <%@ Control Language="VB" ClassName="CH11CVilleWeatherVB" %> <a href="http://www.wunderground.com/US/VA/Charlottesville.html? bannertypeclick=infobox" target="_blank">  <img src="/books/1/505/1/html/2/http://banners.wunderground.com/weathersticker/infobox_both/  language/www/US/VA/Charlottesville.gif"  border="0" alt="Click for Charlottesville, Virginia Forecast" height="108" width="144"/> </a> 

Example 11-2. User control to display book data (.ascx)

 <%@ Control Language="VB" AutoEventWireup="false"  CodeFile="CH11DisplayTabularDataVB.ascx.vb" Inherits="ASPNetCookbook.VBExamples.CH11DisplayTabularDataVB" %> <asp:SqlDataSource  runat="server" /> <asp:GridView  Runat="Server"   AllowPaging="true"   AllowSorting="true"   AutoGenerateColumns="false"   BorderColor="#000080"   BorderStyle="Solid"   BorderWidth="2px" Caption=""   HorizontalAlign="Center"   Width="600px"   PageSize="5"   PagerSettings-Mode="Numeric"   PagerSettings-PageButtonCount="5"   PagerSettings-Position="Bottom"   PagerStyle-HorizontalAlign="Center"   PagerStyle-Css   OnRowCreated="gvData_RowCreated" > <HeaderStyle HorizontalAlign="Center" Css /> <RowStyle css /> <AlternatingRowStyle css /> <Columns> <asp:BoundField DataField="Title"    HeaderText="Title "    SortExpression="Title" /> <asp:BoundField DataField="PublishDate"    HeaderText="Publish Date "    ItemStyle-HorizontalAlign="Center"    SortExpression="PublishDate"    DataFormatString="{0:MMM dd, yyyy}" /> <asp:BoundField DataField="ListPrice"    HeaderText="List Price "    ItemStyle-HorizontalAlign="Center"    SortExpression="ListPrice"    DataFormatString="{0:C2}" /> </Columns>  </asp:GridView> 

Example 11-3. User control to display book data code-behind (.vb)

 Option Explicit On  Option Strict On  Imports System.Configuration.ConfigurationManager  Imports System.Data  Imports System.Data.OleDb Namespace ASPNetCookbook.VBExamples  ''' <summary>  ''' This class provides the code-behind for  ''' CH11DisplayTabularDataVB.ascx  ''' </summary>  Partial Class CH11DisplayTabularDataVB Inherits System.Web.UI.UserControl '''*********************************************************************** ''' <summary> ''' This routine provides the event handler for the page load event. It ''' is responsible for initializing the controls on the page. ''' </summary> ''' ''' <param name="sender">Set to the sender of the event</param> ''' <param name="e">Set to the event arguments</param> Private Sub Page_Load(ByVal sender As Object, _   ByVal e As System.EventArgs) Handles Me.Load  'configure the data source to get the data from the database  'NOTE: This code must be executed anytime the page is rendered  '      including postbacks  dSource.ConnectionString = _ ConnectionStrings("dbConnectionString").ConnectionString dSource.DataSourceMode = SqlDataSourceMode.DataSet  dSource.ProviderName = "System.Data.OleDb"  dSource.SelectCommand = "SELECT Title, PublishDate, ListPrice " & _ "FROM Book " & _  "ORDER BY Title" 'set the data source ID for the GridView  'NOTE: The DataSourceID must be used instead of the DataSource if the  '      automatic sorting/paging in GridView are to be used.  gvData.DataSourceID = dSource.ID If (Not Page.IsPostBack) Then  'perform the initial sort on the first column in ascending order  gvData.Sort(gvData.Columns(0).SortExpression, _  SortDirection.Ascending) End If End Sub 'Page_Load '''*********************************************************************** ''' <summary> ''' This routine provides the event handler for the GridView's row created ''' event. It is responsible for setting the icon in the header row to ''' indicate the current sort column and sort order ''' </summary> ''' ''' <param name="sender">Set to the sender of the event</param> ''' <param name="e">Set to the event arguments</param> Protected Sub gvData_RowCreated(ByVal sender As Object, _    ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs)  Dim index As Integer  Dim col As DataControlField = Nothing  Dim image As HtmlImage = Nothing If (e.Row.RowType = DataControlRowType.Header) Then  'loop through the columns in the gridview updating the header to  'mark which column is the sort column and the sort order  For index = 0 To gvData.Columns.Count - 1 col = gvData.Columns(index) 'check to see if this is the sort column If (col.SortExpression.Equals(gvData.SortExpression)) Then  'this is the sort column so determine whether the ascending or  'descending image needs to be included image = New HtmlImage()  image.Border = 0  If (gvData.SortDirection = SortDirection.Ascending) Then image.Src = "images/sort_ascending.gif" Else  image.Src = "images/sort_descending.gif"  End If 'add the image to the column header  e.Row.Cells(index).Controls.Add(image)  End If 'If (col.SortExpression = sortExpression)  Next index  End If 'If (gvData.SortExpression.Equals(String.Empty))  End Sub 'gvData_RowCreated  End Class 'CH11DisplayTabularDataVB  End Namespace 

Example 11-4. User control to display book data code-behind (.cs)

 using System;  using System.Configuration;  using System.Data;  using System.Data.OleDb;  using System.Web.UI.HtmlControls;  using System.Web.UI.WebControls; namespace ASPNetCookbook.CSExamples {  /// <summary>  /// This class provides the code-behind for  /// CH11DisplayTabularDataCS.aspx  /// </summary>  public partial class CH11DisplayTabularDataCS : System.Web.UI.UserControl  { ///*********************************************************************** /// <summary> /// This routine provides the event handler for the page load event. /// It is responsible for initializing the controls on the page. /// </summary> /// /// <param name="sender">Set to the sender of the event</param> /// <param name="e">Set to the event arguments</param> protected void Page_Load(object sender, EventArgs e) { // configure the data source to get the data from the database  // NOTE: This code must be executed anytime the page is rendered  //  including postbacks  dSource.ConnectionString = ConfigurationManager. ConnectionStrings["dbConnectionString"].ConnectionString;  dSource.DataSourceMode = SqlDataSourceMode.DataSet;  dSource.ProviderName = "System.Data.OleDb";  dSource.SelectCommand = "SELECT Title, PublishDate, ListPrice " + "FROM Book " +  "ORDER BY Title";     // set the data source ID for the GridView  // NOTE: The DataSourceID must be used instead of the DataSource if the  //  automatic sorting/paging in GridView are to be used.  gvData.DataSourceID = dSource.ID; if (!Page.IsPostBack) {  // perform the initial sort on the first column in ascending order  gvData.Sort(gvData.Columns[0].SortExpression, SortDirection.Ascending); } } // Page_Load ///*********************************************************************** /// <summary> /// This routine provides the event handler for the GridView's row created /// event. It is responsible for setting the icon in the header row to /// indicate the current sort column and sort order /// </summary> /// /// <param name="sender">Set to the sender of the event</param> /// <param name="e">Set to the event arguments</param> protected void gvData_RowCreated(Object sender,   System.Web.UI.WebControls.GridViewRowEventArgs e) { DataControlField col = null; HtmlImage image = null; if (e.Row.RowType == DataControlRowType.Header)  {  // loop through the columns in the gridview updating the header to // mark which column is the sort column and the sort order  for (int index = 0; index < gvData.Columns.Count; index++)  { col = gvData.Columns[index]; // check to see if this is the sort column  if (col.SortExpression.Equals(gvData.SortExpression))  { // this is the sort column so determine whether the ascending or // descending image needs to be included image = new HtmlImage(); image.Border = 0; if (gvData.SortDirection == SortDirection.Ascending) { image.Src = "images/sort_ascending.gif"; } else { image.Src = "images/sort_descending.gif";  } // add the image to the column header  e.Row.Cells[index].Controls.Add(image);  } // if (col.SortExpression.Equals(gvBooks.SortExpression))  } // for index  } // if (e.Row.RowType == DataControlRowType.Header)  } //gvData_RowCreated } // CH11DisplayTabularDataCS  } 

Example 11-5. Using regular controls as web parts (.aspx)

 <%@ Page Language="VB" MasterPageFile="~/ASPNetCookbookVB.master" AutoEventWireup="false"  CodeFile="CH11UsingRegularContolsAsWebPartsVB.aspx.vb"  Inherits="ASPNetCookbook.VBExamples.CH11UsingRegularContolsAsWebPartsVB"  Title="Using Server Controls and User Controls as Web Parts" %> <%@ Register TagPrefix="ASPNetCookbook" TagName="CvilleWeather"  src="/books/1/505/1/html/2/~/CH11CVilleWeatherVB.ascx" %>  <%@ Register TagPrefix="ASPNetCookbook" TagName="BookData"  src="/books/1/505/1/html/2/~/CH11DisplayTabularDataVB.ascx" %>  <asp:Content  runat="server" ContentPlaceHolder> <div align="center" >  Using Server Controls and User Controls as Web Parts (VB)  </div> <asp:WebPartManager  runat="server" /> <table width="90%" align="center" border="1" cellpadding="4" cellspacing="0">  <tr>  <td align="right"> <asp:LinkButton  runat="server"    Text="Customize"     Css     OnClick="btnCustomize_Click" />&nbsp;&nbsp; <asp:LinkButton  runat="server"    Text="Reset"    Css    OnClick="btnReset_Click" /> </td> </tr> <tr> <td> <asp:WebPartZone  runat="server"  EmptyZoneText="No Content Selected"   Height="10" HeaderText="Zone 1"   LayoutOrientation="Horizontal"   Css   Padding="6" /> </td> </tr> <tr> <td> <asp:WebPartZone  runat="server"  EmptyZoneText="No Content Selected"   Height="10" HeaderText="Zone 2"   LayoutOrientation="Horizontal"   Css   Padding="6" /> </td> </tr> <tr> <td> <asp:CatalogZone  runat="server"  EmptyZoneText="No Catalog Items"   HeaderCloseVerb-Visible="false"  Css   Padding="6" > <ZoneTemplate>  <asp:PageCatalogPart  runat="server"  Title="Previously Closed Controls" />  <asp:DeclarativeCatalogPart  runat="server"     Title="Available Parts" >  <WebPartsTemplate> <ASPNetCookbook:CvilleWeather    runat="server"   Title="Weather" /> <asp:Calendar  runat="server"   Title="Calendar" />  <ASPNetCookbook:BookData  runat="server"  Title="Book Data" />  </WebPartsTemplate>  </asp:DeclarativeCatalogPart>  </ZoneTemplate>  </asp:CatalogZone> </td> </tr> </table>  </asp:Content> 

Example 11-6. Using regular controls as web parts code-behind (.vb)

 Option Explicit On  Option Strict On Imports System Namespace ASPNetCookbook.VBExamples  ''' <summary>  ''' This class provides the code-behind for  ''' CH11UsingRegularContolsAsWebPartsVB.aspx  ''' </summary>  Partial Class CH11UsingRegularContolsAsWebPartsVB Inherits System.Web.UI.Page '''*********************************************************************** ''' <summary> ''' This routine provides the event handler for the customize button ''' click event. It is responsible for placing the web part manager ''' in catalog mode. ''' </summary> ''' ''' <param name="sender">Set to the sender of the event</param> ''' <param name="e">Set to the event arguments</param> Protected Sub btnCustomize_Click(ByVal sender As Object, _  ByVal e As System.EventArgs) wpm1.DisplayMode = WebPartManager.CatalogDisplayMode End Sub 'btnCustomize_Click '''*********************************************************************** ''' <summary> ''' This routine provides the event handler for the reset button ''' click event. It is responsible for resetting the web part manager ''' personalization data. ''' </summary> ''' ''' <param name="sender">Set to the sender of the event</param> ''' <param name="e">Set to the event arguments</param> Protected Sub btnReset_Click(ByVal sender As Object, _  ByVal e As System.EventArgs) wpm1.Personalization.ResetPersonalizationState() End Sub 'btnReset_Click  End Class 'CH11UsingRegularContolsAsWebPartsVB  End Namespace 

Example 11-7. Using regular controls as web parts code-behind (.cs)

 using System;  using System.Web.UI.WebControls.WebParts; namespace ASPNetCookbook.CSExamples {  /// <summary>  /// This class provides the code-behind for  /// CH11UsingRegularContolsAsWebPartsCS.aspx  /// </summary>  public partial class CH11UsingRegularContolsAsWebPartsCS : System.Web.UI.Page { ///*********************************************************************** /// <summary> /// This routine provides the event handler for the customize button /// click event. It is responsible for placing the web part manager /// in catalog mode. /// </summary> /// /// <param name="sender">Set to the sender of the event</param> /// <param name="e">Set to the event arguments</param> protected void btnCustomize_Click(Object sender,   System.EventArgs e) { wpm1.DisplayMode = WebPartManager.CatalogDisplayMode; } // btnCustomize_Click ///*********************************************************************** /// <summary> /// This routine provides the event handler for the reset button /// click event. It is responsible for resetting the web part manager /// personalization data. /// </summary> /// /// <param name="sender">Set to the sender of the event</param> /// <param name="e">Set to the event arguments</param> protected void btnReset_Click(Object sender,   System.EventArgs e) { wpm1.Personalization.ResetPersonalizationState(); } // btnReset_Click  } // CH11UsingRegularContolsAsWebPartsCS  } 



ASP. NET Cookbook
ASP.Net 2.0 Cookbook (Cookbooks (OReilly))
ISBN: 0596100647
EAN: 2147483647
Year: 2003
Pages: 202

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net