Security and Versioning Models in the Windows Forms Engine Help You Create and Deploy Smart Clients

Chris Sells
This article assumes you're familiar with C#, Windows Forms, and .NET
Level of Difficulty     1   2   3 
SUMMARY Windows Forms applications solve many of the problems inherent in building Web applications the old fashioned way—with HTML. To demonstrate the use of Windows Forms over the Web, the author takes his existing app, Wahoo!, and ports it to Windows Forms. In doing so, he discusses versioning, linked files, security, storage isolation, the deployment model, and everything else you need to get started building your own Windows Forms apps for the Web..
When compared with Visual Basic®, MFC, or even Win32®, HTML is woefully inadequate for building applications. Don't get me wrong. I love the Web. HTML, plug-ins, and browsers have all but eliminated the need for many print, video, and audio media in my life. However, you can't use HTML to build full-featured applications that are optimized for heavy usage. The user interfaces are, by nature, primitive to use and difficult to implement and maintain. And the blame does not rest solely with HTML and JScript®. Building stateful applications on top of the Web's highly concurrent, stateless infrastructure is just hard. You would have thought that long ago people would have given up building HTML applications, at least for situations like the corporate intranet where the client environment is tightly controlled, but so far, HTML still has the mindshare.
The problem, of course, is that darn deployment model. When an IT manager sees that updating a Web site automatically gives users the new version, it's hard to get them to go back to the painstaking process of making sure that all desktops in a corporation have been updated to the latest version of HR452.EXE. Only a technology that provided a deployment as compelling as HTML would stand a chance of unseating it.
Taking up this challenge, .NET marries the latest version of the forms engine that made Visual Basic a corporate development mainstay with the kind of deployment model that turns an IT manager's head. The engine is called Windows Forms and the deployment model is built right into .NET. Here, I'll focus on the elements of Windows Forms deployment itself. For more details on the implementation of Windows Forms, see the MSDN® Magazine article "Windows Forms: A Modern Day Programming Model for Writing GUI Applications" by Jeff Prosise.
Just like a Web application, a Windows Forms application can be deployed on a Web server and executed by merely surfing to a URL. To see for yourself just how easy it is, stop reading and try the following in Visual Studio®.
  1. Using the New Project dialog, choose either a C# or a Basic Windows application and call it DeploymentFun.
  2. Drag and drop some controls from the toolbox, but before going too far, compile your application.
  3. In the shell explorer, navigate to your DeploymentFun/bin folder, right-click on the Debug folder, and choose Properties.
  4. Choose Web Sharing and turn it on, using DeploymentFun as the name of the share.
  5. Now surf to your Windows Forms app using Start | Run and the following URL: http://localhost/DeploymentFun/DeploymentFun.exe.
  6. After basking in the glory of deploying a smart client—a real Windows application over the Web—stop playing around and read the rest of this article.
Figure 1 Wahoo! Using HTML
Figure 1 Wahoo! Using HTML

To test this deployment model, I ported an HTML application to Windows Forms. The application is an implementation of the game of Wahoo! The HTML implementation is shown in Figure 1, while the .NET implementation is shown in Figure 2. Any similarity to any other very popular games that you may already be familiar with is purely intentional.

Figure 2 Wahoo! Using .NET
Figure 2 Wahoo! Using .NET

Both implementations offer identical play. In the Windows Forms version, I wanted to:
  • Make a real Windows-based executable—Wahoo.exe—complete with menu bar, status bar, function key handling, and no browser frame. I wanted it to be downloadable from a Web server.
  • Split the client area into a separate assembly, WahooControl.dll, because you never know when you're going to need a game plugged into your application.
  • Save user settings, such as window size and position, between sessions.
  • Provide access to a Web Service for reporting and retrieving high scores.
  • Cache high score lists on the client hard drive and retrieve them later for viewing.
With the exception of running outside of the browser, these were all features that I could probably have added to my HTML application, given enough time and effort. Frankly, though, I was happy just to get the HTML version running at all. As it was, I had to mandate the use of Internet Explorer 5.0 for the HTML version because I needed a feature that wasn't present in earlier versions (VML support, specifically). In fact, although this is a pretty simple application, it took quite a bit of effort to get the limited object-oriented facilities of JScript and the limited UI facilities of HTML to bend to my will. I'm sure it's no surprise that the better UI, including all of the features mentioned, was easier to implement in Windows Forms. What may be surprising is the small download size of each application. The HTML version (Wahoo.htm plus sblogo.gif) was approximately 58KB while Windows Forms (Wahoo.exe plus WahooControl.dll) was not much larger at 60KB.
I don't know if I could have implemented the extra features in JScript and HTML in two kilobytes, but I do know that it was a whole lot easier to do it in Windows Forms. (Of course, the 20MB .NET Framework runtime could be counted too, but then so should the average Internet Explorer install of 17MB.)

Application Download
Deployment of a .NET application on the server is achieved by simply dropping the app into a virtual directory so that the Web server can hand out the bits on demand. The .NET runtime is not required on the server; nor are Microsoft Internet Information Services (IIS) or Windows. On the client, things are a bit more interesting. When you feed Internet Explorer a URL like
http://localhost/foo/foo.exe
it forms an HTTP request for the foo.exe file to be streamed back to the client:
GET /foo.exe HTTP/1.1
Accept: */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;
                         Q312461; .NET CLR 1.0.3705)
Host: localhost
Connection: Keep-Alive
The response from the server is just a stream of bytes:
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.1
Date: Fri, 01 Feb 2002 02:11:29 GMT
Content-Type: application/octet-stream
Accept-Ranges: bytes
Last-Modified: Fri, 01 Feb 2002 01:41:16 GMT
ETag: "50aae089c1aac11:916"
Content-Length: 45056

<<stream of bytes from foo.exe>>
Besides the bytes themselves, the last modified date/time is also cached. This is used to form a request each subsequent time that the assembly is launched via the same URL:
GET /foo.exe HTTP/1.1
Accept: */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
If-Modified-Since: Fri, 01 Feb 2002 01:41:16 GMT
If-None-Match: "50aae089c1aac11:916"
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;
                         Q312461; .NET CLR 1.0.3705)
Host: localhost
Connection: Keep-Alive
The If-Modified-Since header is kept in the Internet cache and sent back with each request so that if the bits on the server haven't changed, the server can respond with a header that indicates that the cache is still good, reducing the payload that needs to be downloaded to the client.
HTTP/1.1 304 Not Modified
Server: Microsoft-IIS/5.1
Date: Fri, 01 Feb 2002 02:42:03 GMT
ETag: "a0fa92bc8aac11:916"
Content-Length: 0
The bytes themselves are cached in two places: the Internet cache managed by the browser and the .NET download cache. The contents of the download cache can be examined using gacutil.exe /ldl and cleared using gacutil.exe /cdl. If, during your testing, you'd like to ensure that a download happens, make sure to clear out the Internet cache using the Internet control panel, as well as the download cache using gacutil.

Versioning
While I'm on the subject of caching and downloading the latest version, you may be curious about how actual versions affect things. As you may know, you can tag a signed .NET assembly with a specific version using the AssemblyVersion attribute:
// Strong naming is required when versioning
[assembly: AssemblyKeyFileAttribute("wahoo.key")]
[assembly: AssemblyVersionAttribute("1.2.3.4")]
So, if an assembly is versioned, does this version affect the caching and downloading? Well, sometimes.
When you request http://localhost/foo/foo.exe, the runtime doesn't have any idea which version you'd like, so it's just going to ask the Web server for the latest version (as indicated by the If-Modified-Since header sent along with the HTTP request). If the runtime already has the latest version, no download takes place. If the server has a newer binary (based on the date/time stamp on the file), even if that binary is of a lower version number (based on the AssemblyVersion attribute), the lower version will be loaded. In other words, the AssemblyVersion plays no role in what the server considers the latest version of the assembly to be.
But sometimes an assembly may reference other assemblies, either implicitly as part of the manifest or explicitly via the Assembly.Load method. For example, Wahoo.exe references WahooControl.dll. The version of WahooControl.dll that Wahoo.exe compiles against will be the version that the assembly resolver (the part of the .NET runtime responsible for finding code) expects to find at run time. If that version of WahooControl.dll is found in the cache, the assembly resolver will not cause a request to the Web server asking if it has a newer version. Of course, this means that if you'd like the client to have a newer version of a referenced assembly, you'll need to post the updated EXE assembly that refers to it.

Related Files
As I just mentioned, assemblies may reference other assemblies. If a referenced assembly is already present in the Global Assembly Cache (GAC), for example System.Windows.Forms, then the assembly will be loaded from the GAC. If the assembly is not in the GAC, the download cache is checked. If the download cache doesn't contain the assembly, the assembly resolver goes back to the originating server using the application base of the assembly (often called the AppBase). The AppBase is the directory from which the initial assembly was loaded, such as c:/foo or http://localhost/foo/foo/. The AppBase is available using the GetData function of the currently executing application domain (AppDomain), like so:
string appbase = AppDomain.CurrentDomain.BaseDirectory;
For example, when Wahoo.exe needs WahooControls.dll and it's not present in the GAC or the download cache, the assembly resolver will go back to the originating Web server, as you've already seen. Referenced assemblies, however, are not the only files that the assembly resolver will look for when an executable is loaded. In fact, on my machine when I surf to the initial version of the signed Wahoo.exe after the caches have been cleared, 35 requests are made for files, as listed in Figure 3.
The first request makes sense, of course, as it's the assembly I asked for originally. The second request is for a .config file, which is where assembly-specific settings can be set, for example, version mapping and additional probing paths. This is a handy file if you'd like to tailor the resolution policy of your assembly's AppDomain. I recommend the online documentation for more information about just what can go into this file.
The third request is for the WahooControl.dll assembly that Wahoo.exe references, as you'd expect. The next request is for a resources assembly called Wahoo.resources. This assembly is used to resolve resource requests on a per culture basis (which is why the assembly resolver is looking in the subdirectory for the current culture on my machine—United States English). Since the resources assembly is not found where the assembly resolver first looks, it continues to look for this assembly with other names and in other subdirectories over the next 31 requests.
In situations where the latest Wahoo.exe and WahooControl.dll are already cached on my machine, I've seen these additional requests take more than 75 percent of the load time. In a WAN environment, this may not seem like a good idea, but what's happening is actually worthwhile. If I have resources in my application, such as a background image, pulling the resource out of the file requires the use of a resource manager, as shown in the Windows Forms Designer-generated InitializeComponent method:
private void InitializeComponent() {
  System.Resources.ResourceManager resources =
    new System.Resources.ResourceManager(typeof(MainForm));
  •••
  this.game.BackgroundImage = ((System.Drawing.Bitmap)
    (resources.GetObject("game.BackgroundImage")));
  •••
}
When the resource manager is created, it looks in 32 places for the corresponding culture-specific resources before settling on the resources bundled with the assembly. This allows a zero touch model of localization; as you port your application's resources to other cultures, you can drop the assembly.resources.dll into an appropriately named culture directory and these resources will preempt the culture-neutral resource bundled with the assembly. In a LAN or file system environment, this is exactly what you want.
In a WAN environment, these extra round-trips can make for a very poor user experience, especially when the culture-neutral resources are the only resources for your application. In this case, you have a couple of options, one of which is to avoid using the Windows Forms Designer to set properties that use resources such as the BackgroundImage so that the Designer-generated code doesn't create a resource manager. Then, to load resources, you write the code yourself in a culture-neutral way like this:
public MainForm() {
  // Let the Designer-generated code run
  InitializeComponent();

  // Init culture-neutral properties
  this.game.BackgroundImage = new Bitmap(typeof(MainForm),
    "sblogo.gif"); // WARNING: Case sensitive
}
This approach is handy for resources that you know are culture-neutral and that you never want to localize, but it's inconvenient if you've grown attached to the Windows Forms Designer, as I have. For those of you who share my addiction, the resource manager supports an optimization that makes this kind of hand-written code unnecessary in many cases. The NeutralResourcesLanguage attribute is an assembly-level attribute that marks the resources bundled in your application as culture-specific, so that they will be found first without the round-trips to the Web server when launched from that culture:
[assembly: NeutralResourcesLanguageAttribute("en-US")]
This attribute reduces the number of requests from 34 to two when the assembly is launched from a machine with the culture of the assembly, improving load times considerably. If you'd like to let the designer generate the code and reduce round-trips for cultures other than that in which you're writing the application, you can put zero-length files in the first places that the resource manager looks—en-US/appname.resources.dll and en/appname.resources.dll for the U.S.—but this is somewhat of a hack.
If you'd like to reduce the number of requests by one more, you can put up a .config file (although this involves some special entries in your Web.config file if you're serving up your .NET applications via an ASP.NET site—see the code download for an example). The following is the smallest legal .config file:
<configuration></configuration>
Once the .config file has been cached, the .NET v1.0.3705 assembly resolver will not ask for it again, even if there is a newer version on the server. Unfortunately this means that you'd better be darn happy with your application's .config file because the client's Internet cache must be cleared before the .config file will be downloaded again.
Of course, the ultimate reduction of requests is to use no requests at all. By putting Internet Explorer into offline mode (via the File menu), surfing to a smart client will work completely from the cache. If there is no connection, there are no Web Services, but isolated storage (discussed later) is a good place to cache Web Services calls to be made when a connection is reestablished.

Security Zones
As Keith Brown pointed out in his excellent MSDN Magazine article "Security in .NET: Enforce Code Access Rights with the Common Language Runtime,", .NET brings with it a new security model for deployed code. Instead of an assembly getting the permissions of the process running the code (which, let's face it, would give all code full admin rights on most machines), the .NET Code Access Security (CAS) model grants code permissions based on where it's obtained. To view the current permission settings on your machine, use the Microsoft .NET Framework Configuration tool (available in your Administration Tools menu). Drilling into the permission sets for the machine's runtime security policy shows a number of entries, including FullTrust, LocalIntranet, Internet, and so on. Figure 4 compares the LocalIntranet permission set to the Internet permission set.
Assemblies are associated with a permission set in any number of ways, including the publisher, the site, the strong name, and the security zone. Most of the default code groups associate code with a zone. For example, the My_Computer_Zone is associated with the FullTrust permission set and the Local_Intranet_Zone is associated with the LocalIntranet permission set. In release 1.0 of .NET, the Internet_Zone was associated with the Internet permission set, but as of Service Pack 1 of the .NET runtime, code from the Internet_Zone is associated with the Nothing permission set by default. Someday Microsoft may loosen permissions on code from the Internet, but until then they have decided to put security ahead of functionality. However, you can increase permissions for assemblies that you know are safe (as you'll see later).
The zone an assembly is from is determined by the shell and/or the assembly resolver based on the path used to find the assembly, as shown in Figure 5. If an assembly needs to know the zone it's running in, it can access the zone via the Zone class in System.Security.Policy:
                         using System.Security;
  using System.Security.Policy;
  SecurityZone zone =
  Zone.CreateFromUrl(appbase).SecurityZone;
By default, the loaded assembly will get the union of the permissions from all of the code groups to which it belongs and must live within the confines of those permissions. Any attempt to perform an action for which the assembly does not have permission will result in a security exception. The Wahoo and WahooControl assemblies, for example, are designed to work within the restricted set of Internet permissions, which means that it was difficult to implement the functionality I wanted. The challenging areas I encountered included remembering and restoring user preferences, communicating with Web Services, saving and opening files, and hosting the signed WahooControl.dll in Wahoo.exe.

Isolated Storage
Of course, one of the hallmarks of a good application in Windows is that it remembers the preferences set the last time a user ran it. These preferences can be anything from text color to where the main window was when you last saw it. Most applications tend to read these settings from the Registry because it handles per-user settings under HKEY_CURRENT_USER relatively well. The Registry is so overused that it's becoming a maintenance issue, so Windows now has special per-user folders where applications can keep their settings. Unfortunately, applications outside of the MyComputer zone don't have permission to access the file system without user interaction and the Internet permission set doesn't allow an application to write to the file system at all, making the special folders inaccessible to restricted smart clients. For this reason, Microsoft provides isolated storage.
Isolated storage is exposed via the System.IO.IsolatedStorage namespace. It's a special place on the hard drive where even Internet applications can read and write files and directories. The location cannot be known to the application and the size of this area can be restricted (10MB by default in the Internet permission set), but for storing user preferences or medium-sized files, it's fine.
Isolated storage can be segregated into many stores based on the assembly, the AppDomain, and the user, but the only store that Internet applications has permission to access is the user store for the AppDomain. Using this store, the assembly can open or create a file via a stream for reading or writing, as shown in Figure 6.
Once the isolated storage stream is open, the assembly can use it for whatever it likes. The Wahoo assembly, for example, uses the stream to cache and restore window size and position (see Figure 7). The ReadSetting and WriteSetting functions in Figure 7 are helpers for converting simple types to and from strings using TypeConverters (see Figure 8).
TypeConverters are easy to use, but only suitable for simple data. If you'd like to serialize more complicated data, you can send all of the public fields of an object into a stream using the XML serializer defined in the System.Xml.Serialization namespace. If you want protected and private fields serialized too, I would normally point you to the [Serializable] attribute and the formatters in the System.Runtime.Serialization namespace, but that serialization model is unavailable to restricted assemblies. While this can be inconvenient, the unrestricted ability to set an object's protected or private variables is a security hole large enough to give a restricted assembly ownership of the machine.

Communicating with Web Services
Communicating with the user is not the only job of a smart client. Very often they're going to need to communicate to the outside world as well. In the restricted zones, this communication is limited to talking back only to the originating site and only via Web Services. Luckily, the originating site is often what you want to talk to anyway (if that's not the case, you can easily build shims redirecting requests to other servers), and Web Services are flexible enough to handle most communication needs.
In addition to being flexible, Web Services provide a much more manageable model for separating server-side and client-side code. Instead of maintaining client state on the server like a Web application often does, Web Services typically provide a stateless endpoint that receives requests for service. These requests are usually large-grained and atomic to reduce requests and to let the client maintain its own state.
Assuming the Web Service itself was implemented in .NET, generating the client-side proxy code necessary to talk to a Web Service is as easy as adding a Web Reference to your project by pointing Visual Studio .NET at the URL for the Web Service's WSDL. Calling it is tricky as you need to make sure that the URL, which is hardcoded into the generated proxy code, points at the originating server. You can do this by replacing the site in the hardcoded URL with the site that you discover dynamically using the AppDomain's AppBase (see Figure 9).

Reading and Writing Files
Once I'd gotten the current Wahoo! high scores via the Web Service, I found that I wanted to be able to cache them for later access (to savor the brief moment when I was at the top). .NET makes it very easy to read and write files and to show the File Save and File Open dialogs. Unfortunately, only a limited subset of that functionality is available in restricted zones. In the Intranet zone, files can be read and written, but not without interaction with the user. Unrestricted access to the file system is, of course, a security hole on par with buffer overflows and fake password dialogs. To avoid this problem but still allow an application to read and write files, a file can only be opened via the File Save or File Open dialogs. Instead of using these dialogs to obtain a file name from the user, the dialogs themselves are used to open the file (see Figure 10).
Notice in Figure 10 that instead of opening a stream using the SaveFileDialog.FileName property after the user has chosen a file, I call the OpenFile method directly. This gives me an open stream while preventing a restricted program from opening a file without user intervention.
While an application with Intranet permissions can use this technique for both reading and writing files, applications with Internet permissions can only read files. If you'd like your application to dynamically downgrade its capabilities based on what permissions it has, check the permissions by creating a permission object and demanding that permission and catch the security exception if the demand fails. For example, to check whether your application is allowed to show the FileSaveDialog, you use an instance of the FileDialogPermission from the System.Security.Permissions namespace, as shown in Figure 11.
If you wonder what permission you need for a specific call, you should start with the security exception itself, if you can discern the problem from that. Unlike most other exceptions in .NET, the information provided with a security exception is somewhat terse to prevent people with evil intentions from learning too much about an application's implementation. This does tend to make debugging security exceptions a bit harder, though. In these cases, I check the documentation, which is surprisingly good about telling you what permissions are needed and when.

Permissions
However, sometimes the documentation isn't enough. Because of the ever-increasing focus on security by Microsoft, the .NET team spent the two months before the release of .NET (and several more months after that) just tuning and testing security settings. Unfortunately, some of those tunings resulted in documentation changes that are not in the documentation included with the initial release of the product but are available on the Microsoft MSDN site.
One example of this is a permission that an assembly needs to grant to allow it to be called by restricted assemblies. This permission is called AllowPartiallyTrustedCaller (APTC) and it's applied to assemblies via an assembly-level attribute, like so:
[assembly: AllowPartiallyTrustedCallersAttribute]
This permission is only checked once an assembly is signed (which you should always do), but if it is not set, then attempting to use that assembly from an assembly with restricted permissions will cause a security exception. For example, the WahooControl assembly is marked with this attribute to allow it to be hosted by Wahoo. Just remember, it can be very dangerous to mark a signed assembly with the APTC attribute (unsigned assemblies never need to mark themselves APTC). Only a few of the assemblies that come with the .NET are thus marked and those without it cannot be called from restricted zones. If you mark yourself as APTC, you are now taking responsibility for making sure that your assembly cannot be used for evil purposes from restricted zones.
Obviously, debugging and working around security-related issues are the trickiest aspects of smart-client deployment. If you launch a smart client via a URL, you may have noticed that there are no processes running with that name. Instead, each application launched via a URL will get its own copy of IEExec.exe, the process responsible for setting up the appropriate security environment for the application to run in.
The undocumented usage for IEExec.exe is shown in Figure 12, and one common usage of these settings is shown in Figure 13. Notice that the Debug Mode has been set to Program (it defaults to Project), that the Start Application has been set to the full path to IEExec.exe, and that the Command Line Arguments have been set to a URL hosted on the local machine along with a trailing zero to indicate that I need no special flags. If you use 127.0.0.1 to get to the local machine, you'll get default Internet zone permissions when you debug. Likewise, if you use localhost, you'll get default LocalIntranet zone permissions instead.

Figure 13 Debug Settings
Figure 13 Debug Settings

If you'd like to debug without hosting your application in a virtual directory, you can do so using a file URL and an appropriate flags argument. For example, to launch wahoo.exe from the file system using Internet permissions, you can do the following:
   C:/> ieexec.exe file://c:/wahoo/deploy/wahoo.exe    3 3 00
While it's certainly possible to build full-featured applications that run within the confines of a reduced permission set, these restrictions can easily be avoided by granting well-known assemblies increased or even unrestricted permissions. This is especially useful in a corporate Intranet where client machines are configured by the same staff deploying the smart-client applications.
There are a number of ways to increase permissions for an assembly. For example, if you're unhappy with the changes that where made in .NET 1.0 SP1, you can increase or decrease permissions for any zone as a whole using the Adjust .NET Security wizard (available through the Microsoft .NET Framework Wizards item on the Administration Tools menu) as shown in Figure 14.

Figure 14 Risky Internet Security Settings
Figure 14 Risky Internet Security Settings

To be a little less sweeping in your security changes (which I heartily recommend), you can decide to trust all assemblies from a specific site via the Internet Control Panel (see Figure 15). Obviously if you were to choose the full-trust Internet security options that were shown in Figure 14, you'd be putting your systems at risk!

Figure 15 Add Trusted Sites
Figure 15 Add Trusted Sites

If you'd like to adjust permissions for a specific assembly, you can set up a custom code group using the Microsoft .NET Framework Configuration tool or you can use the Trust an Assembly Wizard shown in Figure 16.

Figure 16 .NET Wizards
Figure 16 .NET Wizards

This tool creates a code group named Wizard_N, where the N varies with how many times you run the wizard. When you run this tool, you'll be able to enter the URL to the assembly you'd like to trust, such as
http://trustedmachine/hr452/hr452.exe
Based on that URL, you'll be asked whether you'd like to trust just this assembly, all assemblies from the same publisher, or all of the assemblies with the same public key. To simplify security configuration, it's a good idea to sign all of your assemblies with the same public/private key pair. For example, I signed both Wahoo.exe and WahooControl.dll and can adjust both of their permissions at once by choosing to trust assemblies with the same key (Figure 17).

Figure 17 Trust All with Same Key
Figure 17 Trust All with Same Key

Once you've decided which assemblies to trust in the Trust an Assembly wizard, you'll be asked how much trust you'd like them to have on a sliding scale. It's hard to tell from the user interface (see Figure 18), but the tick marks assign the permission sets FullTrust, LocalIntranet, Internet, and Nothing.

Figure 18 Minimum Security
Figure 18 Minimum Security

With the new code group in place, the next time you surf to the URL, .NET will match the membership condition to the new code group and give the assembly the adjusted permissions.

A New World of Deployment
Smart clients combine the best of the HTML deployment model, the Windows Forms UI development model, and the server-side processing model, without the baggage of HTML UI limitations, the Visual Basic 6.0 single-language restriction, or the need to maintain client state on the server. Especially for the Intranet zone, where you can dictate the client-side configuration, there's no need to restrict yourself to the limitations of HTML in Internet Explorer. Instead, move up to the Microsoft .NET Runtime which offers a whole new deployment model.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值