Be better WPF / MvvmLight developer in 2018

It is 2018, the time of .NET Core, x-plat, clouds, microservices, blockchain and shine of JavaScript. But, there are guys, like me, who still maintain and sometimes develop classic  .NET desktop applications for Windows.

I am not a WPF expert, but I spent a couple of days reviewing, testing and fixing one of our desktop apps and I definitely learned a couple of new tips & tricks that worth to share with other non-experts.

Part #1: General Tips

Tip #1.1: Choose right library/framework

It happened that we use MvvmLight. The library is lightweight and already exists for almost 10 years, MVVM pattern is well-known and lets us keep solution code reasonably well-structured.

But this is definitely not the only choice, there are many other different-purpose libraries and frameworks that may suit you better, especially if you do green-field development. So choose carefully:

Tip #1.2: Distribute using Squirrel.Windows

Installers always were hard, the seamless auto-update process is even harder. But, today, we have the solution that works for simple user-oriented applications that don’t do crazy things during installation. This solution is Squirrel.Windows – an installation and update framework for Windows desktop apps, designed for C# apps.

It’s definitely worth to learn it once and use it for all apps that you develop.

Tip #1.3: Think about monitoring

Aggregated analytics from user’s machine is priceless for successful apps. There are plenty amount of data that can help you deliver better apps:

  1. Crash reports
  2. Application version distribution
  3. User’s count / Active user
  4. Performance / Integrations tracking
  5. Custom events / Logs

It is not always possible to collect all kinds of data from user’s machine, but do it if you can. There are a couple of services that may help you, like Application Insights, HockeyApp and others.

Tip #1.4: Use the full power of IDE

Learn tools that MS baked for you and use them

Tip #1.5: Debug Data Binding Issues

When data bindings do not play nice you have a possibility to debug. It is not super intuitive, but there are ways to step into the binding process to better figure out what is actually going on. Check this nice article from Mike Woelmer – How To Debug Data Binding Issues in WPF 

Part #2: MVVM Light – Code Tips

C# quickly evolves over time, more and more features become available to us. It is not always obvious how to use new async code with an old API.

Tip #2.1: “New” INotifyPropertyChanged syntax

I think almost any WPF developer knows how to implement INotifyPropertyChanged interface

public class MyViewModel : INotifyPropertyChanged
{
    private string _isBusy;
    public event PropertyChangedEventHandler PropertyChanged;

    public MyViewModel() {}

    public string IsBusy
    {
        get { return _isBusy; }
        set
        {
            _isBusy = value;
            OnPropertyChanged("IsBusy");
        }
    }

    protected void OnPropertyChanged(string name)
    {
        if (PropertyChanged == null)
            return;
        PropertyChanged(this,
            new PropertyChangedEventArgs(name))
    }
}

Using MVVM Light you can do way shorter (such syntax probably exists for a while, but I discovered it only recently)

public class MyViewModel : ViewModelBase
{
    public MyViewModel() {}

    private string _isBusy;
    public string IsBusy
    {
        get => _isBusy;
        set { Set(() => IsBusy, ref _isBusy, value) }
    }
}

All property change events will happen under the hood of Set method. Also, Set method returns true when the value changed so you can use it to do additional actions on property change.

private string _isBusy;
public string IsBusy
{
    get => _isBusy;
    set {
        if (Set(() => IsBusy, ref _isBusy, value)) {
        // Do whatever you need on update
        }
    }
}

Update from Chris Jobson:

There are overloads that allow us to omit first argument – propertyExpression. In this case [CallerMemberName] will be used as the property name, so the code will be even shorter. Not bad for 2018 =)

private string _isBusy;
public string IsBusy
{
    get => _isBusy;
    set => Set(ref _isBusy, value);
}

Tip #2.2: Async to Action glue

C# async was designed to be better compatible with old APIs and consume Action or delegate. Also, it is one of the reasons why async void exists in the language, but we should always use async Task in our own code.

Two following casts are valid

Action task = async () => await Task.Yield();
Func task2 = async () => await Task.Yield();

Read “Do async lambdas return Tasks?” to better understand what’s actually going on here. It means that you can pass your async method as Action to RelayCommand.

new RelayCommand(async() => await Download());

TBH, you should use it like this (explanation in the next tip)

new RelayCommand(async() => await Download(), keepTargetAlive:true);

Tip #2.3: Do not use lambdas with RelayCommand

Lambdas as a parameter for RelayCommand is a bad idea unless you know what can go wrong and use the latest version of MvvmLight.

Actually, I have spent almost 2 days of my life to figure out why at some point of time several buttons in our application stopped working, even though all commands defined in the ViewModel are read-only and assigned once in the constructor.

We had simple commands that do some trivial actions on click, so the developer decided to use lambda in command declaration to save space and simplify the code.

new RelayCommand(() => IsBusy = true);

The code looks simple and correct, but RelayCommand under the hood stores only weak reference to the delegate and any GC cycle can recycle local lambda function. So at some point in time (after next cycle of GC) RelayCommand may not find delegate to call and nothing will happen after the click. For a deeper analysis of this behavior, you can read “RelayCommands and WeakFuncs“.

At the time of writing this post, the issues in MvvmLight library were fixed (Using RelayCommand and Messenger (and WeakAction) with closures) and released in version 5.4.1. But fix does not apply by default.

If you really want to use lambdas with RelayCommandMessenger you should manually set keepTargetAlive:true (false by default), but probably better do not use them at all.

new RelayCommand(() => IsBusy = true, keepTargetAlive:true);

P.S. Worth to mention that Laurent Bugnion has the course on Pluralsight “MVVM Light Toolkit Fundamentals” that provides detailed MVVM Light overview.

SuperSDG2: The maze game (С++ & OpenGL)

I have found that I have a large set of interesting and maybe sometimes useful applications. I have been programming for 14 years, I started from Pascal and tried Delphi, C++, Java, Perl, Clipper, FoxPro, Lua , JavaScript, C#, Octave, Python, Scala, R, F# (and may be something else that I currently do not remember). Through the years I created set of programs that may be interesting for someone else. These programs may look simple for experts, but I hope that they can be useful for beginners. So, I would like to add a new rubric to my blog, called “Apps”, where I will try to collect and share as much as I can find)

This post is about second version of the small maze game that called SuperSDG2, which was created in 2005 using C++, OpenGL(glut32) and a bit of passion. In the game, you are a lonely red ball in the large terrible maze. Your only wish is to find an exit from the random-generated maze. It is strange, but exit is a  yellow ball ;). The source code and binaries of this application are available on GitHub.

Control keys:

  • Left/Right/Up/Down – move actions
  • A/S – rotate camera
  • Z/X – up/down camera
  • R – reset camera position
  • Esc/Q – exit

Feel free to download binaries, play and modify the source code.

SuperSD2_1

SuperSD2_2

/********************************
* SuperSDG v2.2 release	
* Copyright (c) 2002-2008 Ravent
* All rights reserved
*********************************/
#pragma comment (lib,"glut32.lib")
#pragma comment (lib,"glaux.lib")

#include "glaux.h"
#include "glut.h"
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#define _USE_MATH_DEFINES
#include <math.h>
#include <time.h>

const int sMax=6, m=40, mm=m+1, direction_parts=36;
int cur_direction=0;
double distance=4.;

unsigned textureId = -1, texFloor = -1;
AUX_RGBImageRec *localTexture = NULL, *localFloor = NULL;
int glWin,pathLen,myLen=0;

struct Tpos
{char x,y;};
struct Player
{
    int x,y,z;
    int dx,dz;
    bool isGo;
}player,ex;

struct Maps
{int x,z;} map;

char data[m+2][m+2], cp[m+2][m+2];

float	lightAmb [] = { 0.03, 0.03, 0.03 };
float	lightDif [] = { 0.95, 0.95, 0.95 };
float	lightPos [] = { (int)m/2,  7,  (int)m/2 };

void display(void);
void halt(bool f=false);

void drawFloor(GLfloat x1, GLfloat x2, GLfloat z1, GLfloat z2, unsigned texture=texFloor)
{
    glBindTexture ( GL_TEXTURE_2D, texture );
    glBegin(GL_POLYGON);
        glNormal3f( 0.0, 1.0, 0.0);
        glTexCoord2f(0,0);
        glVertex3f( x1, 0, z2 );
        glTexCoord2f(1,0);
        glVertex3f( x2, 0, z2 );
        glTexCoord2f(1,1);
        glVertex3f( x2, 0, z1 );
        glTexCoord2f(0,1);
        glVertex3f( x1, 0, z1 );
    glEnd();

}

void drawBox (GLint j, GLint i, unsigned texture=textureId)
{
    GLfloat x1=i, x2=i+1, y1=0, y2=1, z1=j, z2=j+1;
    glBindTexture ( GL_TEXTURE_2D, texture );

    if ((j==map.z+1)||(data[j-1][i]!='x'))
    {
    glBegin(GL_POLYGON); // Back
        glNormal3f( 0.0, 0.0, -1.0);
        glTexCoord2f(0,0);
        glVertex3f( x2, y1, z1 );
        glTexCoord2f(1,0);
        glVertex3f( x1, y1, z1 );
        glTexCoord2f(1,1);
        glVertex3f( x1, y2, z1 );
        glTexCoord2f(0,1);
        glVertex3f( x2, y2, z1 );
    glEnd();
    }
    if ((j==map.z-1)||(data[j+1][i]!='x'))
    {
    glBegin(GL_POLYGON); // Front
        glNormal3f( 0.0, 0.0, 1.0);
        glTexCoord2f(0,0);
        glVertex3f( x1, y1, z2 );
        glTexCoord2f(1,0);
        glVertex3f( x2, y1, z2 );
        glTexCoord2f(1,1);
        glVertex3f( x2, y2, z2 );
        glTexCoord2f(0,1);
        glVertex3f( x1, y2, z2 );
    glEnd();
    }
    if ((i>0)&&(data[j][i-1]!='x'))
    {
    glBegin(GL_POLYGON); // Left
        glNormal3f( -1.0, 0.0, 0.0);
        glTexCoord2f(0,0);
        glVertex3f( x1, y1, z1 );
        glTexCoord2f(1,0);
        glVertex3f( x1, y1, z2 );
        glTexCoord2f(1,1);
        glVertex3f( x1, y2, z2 );
        glTexCoord2f(0,1);
        glVertex3f( x1, y2, z1 );
    glEnd();
    }
    if ((i<map.x)&&(data[j][i+1]!='x'))
    {
    glBegin(GL_POLYGON); // Right
        glNormal3f( 1.0, 0.0, 0.0);
        glTexCoord2f(0,0);
        glVertex3f( x2, y1, z2 );
        glTexCoord2f(1,0);
        glVertex3f( x2, y1, z1 );
        glTexCoord2f(1,1);
        glVertex3f( x2, y2, z1 );
        glTexCoord2f(0,1);
        glVertex3f( x2, y2, z2 );
    glEnd();
    }
    glBegin(GL_POLYGON); // Top
        glNormal3f( 0.0, 1.0, 0.0);
        glTexCoord2f(0,0);
        glVertex3f( x1, y2, z2 );
        glTexCoord2f(1,0);
        glVertex3f( x2, y2, z2 );
        glTexCoord2f(1,1);
        glVertex3f( x2, y2, z1 );
        glTexCoord2f(0,1);
        glVertex3f( x1, y2, z1 );
    glEnd();
}

void animate()
{
    if ((player.x == ex.x)&&(player.z==ex.z))
    {
        halt(true);
    };
    if (player.isGo==true)
    {
        if (player.dx>0)	player.dx+=1; else
        if (player.dz>0)	player.dz+=1; else
        if (player.dx<0)	player.dx-=1; else
        if (player.dz<0)	player.dz-=1;
        if ((player.dx>=sMax)||(player.dz>=sMax))
        {
            player.isGo=false;
            if (player.dx>0)	player.x+=1;
            if (player.dz>0)	player.z+=1;
            player.dx=0; player.dz=0;
        }else
        if ((player.dx<=-sMax)||(player.dz<=-sMax))
        {
            player.isGo=false;
            if (player.dx<0)	player.x-=1;
            if (player.dz<0)	player.z-=1;
            player.dx=0; player.dz=0;
        }
    }
    glutPostRedisplay();
}

void init()
{
    glClearColor( 0.0, 0.0, 0.0, 1.0 );
    glEnable( GL_DEPTH_TEST );
    glEnable( GL_TEXTURE_2D );
    glEnable( GL_CULL_FACE );
    glPixelStorei ( GL_PACK_ALIGNMENT, 1 );
    glPixelStorei ( GL_UNPACK_ALIGNMENT, 1 );
    glShadeModel (GL_SMOOTH);
    glLightfv    ( GL_LIGHT0, GL_AMBIENT,  lightAmb );
    glLightfv    ( GL_LIGHT0, GL_DIFFUSE,  lightDif );
    //glLightfv    ( GL_LIGHT0, GL_POSITION, lightPos );
    glEnable ( GL_LIGHT0 );
    glEnable ( GL_LIGHTING );
    player.dx=0; player.dz=0; player.isGo=false;
    glEnable(GL_COLOR_MATERIAL);
}

void display()
{
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    gluLookAt(player.x+(1.0*player.dx/sMax)+0.5f+3*cos(M_PI_2+cur_direction/double(direction_parts)*2.*M_PI),player.y+distance,player.z+(1.0*player.dz/sMax)+0.5f+3*sin(M_PI_2+cur_direction/double(direction_parts)*2.*M_PI),
              player.x+(1.0*player.dx/sMax)+0.5f,player.y+0.5f,player.z+(1.0*player.dz/sMax)+0.5f,
              0,1,0);

    for (int i=0;i<map.x;i++)
        for (int j=0;j<map.z;j++)
            if (data[j][i] == 'x')
            {
                drawBox(j,i);
            } else {
                drawFloor(i,i+1,j,j+1);
            }

    glPushMatrix(); 
    glTranslatef ( player.x+(1.0*player.dx/sMax)+0.5f, player.y+0.5f, player.z+(1.0*player.dz/sMax)+0.5f);
    glColor3d(1,0,0);
    glutSolidSphere(0.5,100,100);
    glColor3d(1,1,1);
    glPopMatrix(); 

    glPushMatrix(); 
    glTranslatef ( ex.x +0.5f, ex.y+0.5f, ex.z+0.5f);
    glColor4d(1,1,0.,0.4);
    glutSolidSphere(0.5,100,100);
    glColor3d(1,1,1);
    glPopMatrix(); 

    glutSwapBuffers();
}

void reshape ( int w, int h )
{
    glViewport( 0, 0, (GLsizei)w, (GLsizei)h);
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    gluPerspective( 60.0, (GLfloat)w/(GLfloat)h, 1.0, 60.0);

    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    gluLookAt(0,0,25,0,0,0,0,1,0);
}

void key( unsigned char key, int x, int y)
{
    if ( key=='q' || key=='Q' || key == 27) halt(false);
    if ( key=='a' || key=='A' ) { cur_direction--; if (cur_direction<0) cur_direction+=direction_parts;}
    if ( key=='s' || key=='S' ) { cur_direction++; if (cur_direction==direction_parts) cur_direction=0;}
    if ((key=='z' || key=='Z') && (distance<58.) ) distance+=0.25;
    if ((key=='x' || key=='X') && (distance>3.) ) distance-=0.25;
    if ( key=='r' || key=='R' ) { cur_direction=0; distance=4.;}
}

const int move[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
const int move_key[4] = {GLUT_KEY_UP, GLUT_KEY_LEFT, GLUT_KEY_DOWN, GLUT_KEY_RIGHT };
bool good_move(int z, int x){
    return (0<=z && 0<=x && z<map.z && x<map.x && data[z][x]!='x');
}

void keys( int key, int x, int y)
{
    if (player.isGo) return;
    int dir=int(((direction_parts-cur_direction-1)/double(direction_parts))*4.+0.5);
    for (int i=0; i<4; i++)
        if ( key == move_key[i] ) {
            int newz=player.z+move[(dir+i)%4][0];
            int newx=player.x+move[(dir+i)%4][1];
            if (good_move(newz,newx)) {
                player.isGo=true;
                player.dz+=move[(dir+i)%4][0];
                player.dx+=move[(dir+i)%4][1];
                myLen++;
            }
        }
}

void genMap(void);

int main (int argc, char** argv)
{
    genMap();
    glutInit(&argc,argv);
    glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH );
    glutInitWindowSize(1024,768);

    glWin = glutCreateWindow("SuperSDG 2");
    init();

    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutKeyboardFunc(key);
    glutSpecialFunc(keys);
    glutIdleFunc(animate);

    localTexture = auxDIBImageLoad(L"1.bmp");
    glGenTextures (1,&textureId);
    glBindTexture (GL_TEXTURE_2D,textureId);
    glPixelStorei (GL_UNPACK_ALIGNMENT,1);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,GL_REPEAT);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,GL_REPEAT);
    gluBuild2DMipmaps( GL_TEXTURE_2D, GL_RGB, localTexture->sizeX, localTexture->sizeY, GL_RGB, GL_UNSIGNED_BYTE, localTexture->data);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    localFloor = auxDIBImageLoad(L"2.bmp");
    glGenTextures (1,&texFloor);
    glBindTexture (GL_TEXTURE_2D,texFloor);
    glPixelStorei (GL_UNPACK_ALIGNMENT,1);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,GL_REPEAT);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,GL_REPEAT);
    gluBuild2DMipmaps( GL_TEXTURE_2D, GL_RGB, localFloor->sizeX, localFloor->sizeY, GL_RGB, GL_UNSIGNED_BYTE, localFloor->data);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    glutFullScreen();
    glutMainLoop();
    return 0;
}

int step(int y, int x)
{
    int res=0;
    if ((data[y][x]=='x')) res++;
    if ((y<mm)&&(data[y+1][x]=='x')) res++;
    if ((y>0 )&&(data[y-1][x]=='x')) res++;
    if ((x<mm)&&(data[y][x+1]=='x')) res++;
    if ((x>0 )&&(data[y][x-1]=='x')) res++;
    return res;
}

int stepC(int y, int x)
{
    int res=0;
    if ((cp[y][x]=='x')) res++;
    if ((y<mm)&&(cp[y+1][x]=='x')) res++;
    if ((y>0 )&&(cp[y-1][x]=='x')) res++;
    if ((x<mm)&&(cp[y][x+1]=='x')) res++;
    if ((x>0 )&&(cp[y][x-1]=='x')) res++;
    return res;
}

void DataToCp(void)
{
    for(int j=1;j<=mm;j++)
        for (int i=1;i<=mm;i++)
            cp[j][i]=data[j][i];
}

void fill (int y,int x,Tpos *v, int *l)
{
    int st=1,en=0;
    int G[mm*mm],Ll[mm*mm];
    G[0]=y*mm+x; Ll[0]=0;
    cp[y][x]='x';
    while (st!=en)
    {
        if ((G[en]%mm+1<mm)&&(cp[(int)G[en]/mm][G[en]%mm+1]=='.'))
        {
            G[st]=G[en]+1;
            Ll[st]=Ll[en]+1;
            cp[(int)G[st]/mm][G[st]%mm]='x';
            st++;
        }
        if ((G[en]%mm-1>0)&&(cp[(int)G[en]/mm][G[en]%mm-1]=='.'))
        {
            G[st]=G[en]-1;
            Ll[st]=Ll[en]+1;
            cp[(int)G[st]/mm][G[st]%mm]='x';
            st++;
        }
        if ((((int)G[en]/mm)+1<mm)&&(cp[((int)G[en]/mm)+1][G[en]%mm]=='.'))
        {
            G[st]=G[en]+mm;
            Ll[st]=Ll[en]+1;
            cp[(int)G[st]/mm][G[st]%mm]='x';
            st++;
        }
        if ((((int)G[en]/mm)-1>0)&&(cp[((int)G[en]/mm)-1][G[en]%mm]=='.'))
        {
            G[st]=G[en]-mm;
            Ll[st]=Ll[en]+1;
            cp[(int)G[st]/mm][G[st]%mm]='x';
            st++;
        }
        en++;
    }
    int rnd=rand()%5+1;
    (*v).x =(int) G[en-rnd] % mm;
    (*v).y =(int) G[en-rnd] / mm;
    *l=Ll[en-rnd];
}

bool isGood(Tpos a)
{
    DataToCp();
    int k=0,i,j,lt;
    Tpos temp;
    cp[a.y][a.x]='x';
    if ((step(a.y,a.x)>3)||(step(a.y+1,a.x)>3)||(step(a.y-1,a.x)>3)||(step(a.y,a.x+1)>3)||(step(a.y,a.x-1)>3))
        return false;
    for(j=1;j<=m;j++)
        for(i=1;i<=m;i++)
            if (cp[j][i]=='.')
            {
                if (k>0)
                    return false;
                fill(j,i,&temp,&lt);
                k++;
            }
    return true;
}

void genMap()
{
    int i,j;
    map.x=m+2; map.z=m+2;
    printf("Loading... Please Wait.\n");
    srand( (unsigned)time( NULL ) );
    for (j=0;j<map.z;j++)
        for (i=0;i<map.x;i++)
        {
            if ((i==0)||(j==0)||(i==map.x-1)||(j==map.z-1))
                data[j][i]='x';
                else
                data[j][i]='.';
        }

    int k=0; 
    Tpos t;
    while (k<(int)m*m/2.5)
    {
        t.x = rand()%mm+1;
        t.y = rand()%mm+1;
        if ((data[t.y][t.x]=='.')&&(isGood(t)))
        {
            data[t.y][t.x]='x';
            k++;
        }
    }
    for (j=1;j<=m;j++)
        for (i=1;i<=m;i++)
            if ((data[j][i]=='x')&&(step(j,i)>3))
            {
                data[j][i]='.';
            }
    for (j=1;j<=m;j++)
        for (i=1;i<=m;i++)
            if ((data[j][i]=='.')&&(step(j,i)<2))
            {
                t.x = i; t.y=j;
                if (isGood(t))
                    data[j][i]='x';
            }

    Tpos ps[11]; k=0;
    while (k<=10)
    {
        t.x = rand()%mm+1;
        t.y = rand()%mm+1;
        if (data[t.y][t.x]=='.')
        {
            ps[k]=t;
            k++;
        }
    }
    k=rand()%11;
    player.x=ps[k].x;
    player.z=ps[k].y;
    DataToCp();
    fill(ps[k].y,ps[k].x,&t,&pathLen);
    ex.x=t.x;
    ex.z=t.y;

    printf("Loading complete.\n");
}

void halt(bool f)
{
        glutDestroyWindow(glWin);
        printf("-----------------------------------------------------\n");
        printf("#                    SuperSDG 2.2                   #\n");
        if (f==true)
        {
        printf("-----------------------------------------------------\n");
        printf("# The shortest path %d.                            #\n",pathLen);
        printf("# Your path %d.                                    #\n",myLen);
        printf("# Congratulate you passed the game.                 #\n");
            if (  1.2*pathLen >= myLen )
            {
                printf("-----------------------------------------------------\n");
                printf("#     YOU ONE OF THE BEST PLAYER OF THE WORLD.      #\n");
            }
        }
        printf("-----------------------------------------------------\n");
        printf("# Developers:                                       #\n");
        printf("#        Ravent     - programmer.                   #\n");
        printf("# Series found:                                     #\n");
        printf("#        Ravent     - Sergey Tihon                  #\n");
        printf("#        MasterZerg - Dima   Rudol                  #\n");
        printf("-----------------------------------------------------\n");
        printf("#          Copyright (c) Ravent 2002-2008.          #\n");
        printf("#               All rights reserved                 #\n");
        printf("-----------------------------------------------------\n");
        getch();
        exit(0);
}